@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
package/WORK_ORDERS.md DELETED
@@ -1,148 +0,0 @@
1
- # WORK_ORDERS.md — ui-core-micha
2
-
3
- Work-order register for this repo. Lightweight directory (not the full orders):
4
- one row per WO with its implementation status. Convention, schema, and maintenance
5
- rules are defined centrally in `webapps/AGENTS.md` → "Work-Order Register".
6
-
7
- ## Workstream prefixes
8
-
9
- | Prefix | Workstream |
10
- |---|---|
11
- | `ONB-*` | Onboarding wizard (steps, conditions, persistence) |
12
- | `PERF-*` | Performance improvements |
13
- | `NOTIF-*` | Shared notifications platform (dcm register is canonical; this repo's rows are its ucm-side WOs) |
14
- | `CHART-*` | Shared chart / data-viz kit (`ChartFrame` + MUI X-Charts presets + neutral palette/formatters) |
15
- | `THEME-*` | Shared MUI theme baseline (`createAppTheme` factory, token surfaces, completeness assertion) |
16
- | `MSG-*` | Shared messaging platform, Phase B (dcm register is canonical; this repo's rows are its ucm-side WOs) |
17
- | `CHAT-*` | Shared AI chat surface (chat list, streaming thread, sources, model picker). The django-core-micha register is canonical for the core (`DCM-CHAT-*`); this repo's rows are its ucm-side WOs |
18
- | `DX-*` | Developer experience / repo tooling (ucm-local prefix; unrelated to `django-core-micha`'s own `DX-*`) |
19
- | `AUTH-*` | Auth components UX (login/password/MFA/profile forms, auth error i18n, and the account screen's own surfaces) |
20
- | `I18N-*` | The shared kit's i18n **contract** — how a consumer registers ucm's strings, and what happens when it forgets. Distinct from `AUTH-*`, which owns the auth strings themselves |
21
- | `PRIM-*` | Shared dashboard/data primitives — small presentational components that are neither chart (`CHART-*`) nor app chrome (`SHELL-*`) |
22
- | `SHELL-*` | Shared app chrome and navigation shells (`UserMenu`, mobile bottom nav, secondary-navigation shell) — used since `SHELL-1`, documented here 2026-08-11 |
23
- | `TEST-*` | Consumer integration checks the kit ships for apps to run in their own suites |
24
-
25
- Introduce a new prefix when none fits and add it here. New WOs always get a
26
- prefixed ID; never reuse a bare flat number across workstreams.
27
-
28
- ## Register
29
-
30
- | ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
31
- |---|---|---|---|---|---|---|
32
- | UCM-CHAT-2 | [Readable driver names in the chat model picker](work-orders/UCM-CHAT-2.md) | Picker group headings (and any other driver display) show translated names for `claude_cli`, `anthropic_api`, `openai_api` instead of the raw ids; unknown ids fall back to the raw id. Correction to UCM-CHAT-1, found at acceptance against its prototype. | 2026-10-15 | done | f35b6ea | Tier 3 · Codex gpt-6-luna dispatched once, RESULT: DONE; Orchestrator reviewed and added the real-i18n test. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui · 3 raised · 2 accepted · worst accepted: defect (accepted: picker tests only saw mocked keys, effort-via-t() unpinned; both fixed with tests/chatDriverNames.test.jsx on the real translations; rejected: 'divider shows raw driver id' raised by two lenses, false: the divider shows model and effort only, no driver). Batch also covered the 3.14.1 delta da068fb and a tests lens given the actual test files (operator request 2026-10-15). Scoped tests 63 pass, tsc clean. Published as 3.14.2 by push to main; cockpit/CP-CHAT-4 pins it. Gate #1 recorded at ui-core-micha@8a9737b. |
33
- | UCM-CHAT-1 | [Shared AI chat surface: chat list, streaming thread, sources, model and effort picker](work-orders/UCM-CHAT-1.md) | Workspace plus individually exported parts against the DCM-CHAT-1 contract; Markdown via `react-markdown` + `remark-gfm` (allow-listed: no raw HTML, no images); per-chat model and effort picker; source chips; edge states. Prototype [work-orders/assets/UCM-CHAT-1-ai-chat.html](work-orders/assets/UCM-CHAT-1-ai-chat.html). | 2026-10-15 | done | 2dede28 | Tier 3 · Prototyp. Codex gpt-6-luna dispatched once, returned BLOCKED with a skeleton; Orchestrator (Claude) completed it, so author = Claude. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui · 18 raised · 13 accepted · worst accepted: defect. Last fix batch (reconnect chip, status-first load, scroll pinning, effort i18n) re-checked by scoped tests only, not re-reviewed. Scoped chat tests 64 pass, tsc build clean; packageTreeShaking fails locally on Windows symlink EPERM, runs in publish.yml (tests before publish). Wire shape (URLs, frame/field names) pinned from maana in src/chat/wire.js because DCM-CHAT-1 is unpublished. Rendered 375/1280 side-by-side runs in cockpit/CP-CHAT-4. Docs row: webapp-management 955c2e8. Published as 3.14.0 by push to main. Gate #1 approved by the operator 2026-10-15 (click in session local_03d1141f-9577-4c9e-8ccf-3343b3b34a19). post-landing re-review of fix batch (operator request 2026-10-15): codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui + sec (Markdown path, socket/url) · 4 raised · 2 accepted · worst accepted: defect; accepted: overlapping older-turn loads, trailing separator in picker label (fixed in da068fb, 3.14.1, +4 tests); rejected: wsUrl/apiBase scheme checks (developer-supplied host config, not user input) x2; tests lens could not ground a verdict (no test files in its material); the 3.14.1 delta itself is covered by scoped tests (68 pass) only. Markdown path: no raw HTML, image fetch or active link reachable. 3.14.1 delta da068fb and the tests lens were covered in the UCM-CHAT-2 batch (tests lens given the test files of 2dede28, 3.14.1 and UCM-CHAT-2): 0 defects against the 3.14.1 delta (single-flight guard and picker label are pinned by assertions); 1 raised, 1 accepted (effort-via-t() was not pinned; fixed with tests/chatDriverNames.test.jsx), worst accepted: defect. |
34
- | UCM-AUTH-13 | [Preserve profile edits during refresh](work-orders/UCM-AUTH-13.md) | Keep unsaved names and consent for the same person; reset at account boundaries. Patch release and Spesix consumption approved. | 2026-10-14 | done | f29d1dd | Tier 3. Gate #1 approved by the operator in the Spesix orchestration chat on 2026-10-14. Fresh native configured codex/gpt-6-luna/high author `/root/ucm_auth13_author`; workspace-root cwd bootstrap, absolute target paths; effective provider/cwd/sandbox not exposed. Foreign scratchpad excluded. review: codex/gpt-6-luna · lenses: envelope,regression,duplication,tests,ui,security · 2 raised · 0 accepted · worst accepted: none. Parent 11 focused tests and TypeScript build passed. Released3.13.2: publish CI38640512864 build/90 test files/publish passed, registry confirmed. Spesix a30de18 installed-package consumer8 tests passed; separate SPX-DEP-5 owns real browser CI38641555651. Detailed dispositions in WO C. |
35
- | UCM-AUTH-12 | [Optional account profile statement text](work-orders/UCM-AUTH-12.md) | AccountPage showProfileStatements defaults true; false suppresses statement panels/requests while preserving both consent controls and their payload. Minor release 3.13.0, consumed by innoservice INNO-FIX-13. | 2026-10-13 | done | 97b7d83 | Tier 3 (shared-core account surface). Gate #1 explicitly approved in the innoservice Codex chat on 2026-10-13, including release and app pin bump. Separate configured Luna implementation and independent reviews; existing default and consent behavior are invariants. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui, security · 0 raised · 0 accepted · worst accepted: none. Parent scoped tests: AccountPage.profileStatements.test.jsx + AccountPage.test.jsx, 6 passed, correct kit cwd, maxWorkers2. Native author attempt EPERM before collection; parent gate green. Reviewed additive API/default and package release metadata; main push triggers the existing release build/suite/publish. Released 3.13.0: CI38422682968 build, release suite and npm Trusted Publishing all succeeded. App consumption/capture remains tracked under INNO-FIX-13. |
36
- | UCM-SHELL-8 | SectionNav lists contain direct button children | Grouped and overview entries need genuine list items containing the existing navigation buttons, preserving selection, drawer behavior and presentation. | 2026-10-13 | done | `d399f51` | Tier 3. Gate #1 approved by the operator on 2026-10-13: structural correction, tests/review, patch release and innoservice pin bump. Full scope and context: [work-orders/UCM-SHELL-8.md](work-orders/UCM-SHELL-8.md). Original CI38387014900/source9937adb evidence remains in innoservice/work-orders/assets/INNO-FIX-11-axe-nodes.json; CI38425506007/sourceebc35e1 reproduces the three account list violations on 3.13.0, recorded in INNO-FIX-11-candidate-b-axe-nodes.json. Initial native author /root/kit_sectionnav requested codex/gpt-6-luna; effective native metadata unexposed. Source and two focused test cases delivered; author run EPERM before collection, zero tests. Parent tests and independent review pending. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests + UI · 1 raised · 0 accepted · worst accepted: none. Disposition in WO Part C: fabricated class-stub identity demand rejected against the real revert failure and preserved existing behavior checks. Parent 21 scoped tests passed (maxWorkers=2), TypeScript build exit 0. Core source accepted; publication, app pin and fresh consumer capture pending. Closing proof:3.13.1 published CI38435441867 (744tests89files/build/npm success); app exact pin/sourcec527918, CI38444366349 account375/1280 both axe0,console/network0,rubric22yes and parent actual-image inspection; all three list violations gone. No source change since reviewed d399f51. |
37
- | UCM-THEME-17 | Decide a less compressed baseline type scale | Design round (design-language process): inventory, instrument with 2-3 candidate scales next to today's baseline and MUI on real screens (hram, a survey site, jg-ferien, kerzenziehen), value rounds, frozen sheet. Measured 2026-10-04: page/card/section titles differ by 1-3 px, hierarchy rests on weight alone. Full order [`work-orders/UCM-THEME-17.md`](work-orders/UCM-THEME-17.md) | 2026-10-04 | done | `68dfc6a`, `998e36c`, `cb9dd89`, `c452224` | Tier 2 (document only) · operator decision 2026-10-04: baseline finding, not per-app overrides · implementation order follows the freeze · survey_app SVA-THEME-1 typography revert held until then · **frozen 2026-10-04** after two value rounds: candidate B, mobile h4 23, h1-h3 36/32/28; value source [`docs/TYPE-SCALE.md`](docs/TYPE-SCALE.md) · review: none, document order, the operator freeze is the gate · implementation UCM-THEME-18 |
38
- | UCM-THEME-18 | Ship the frozen baseline type scale | `tokens.js` typography per `docs/TYPE-SCALE.md`: h1-h3 36/32/28, h4 24 (23 below `sm`), h5 20, subtitle1 13; light fixture typography entries; minor release 3.12.0. Full order [`work-orders/UCM-THEME-18.md`](work-orders/UCM-THEME-18.md) | 2026-10-04 | 5be83d5 | | Tier 3 (shared-core) · reviewer + ui_reviewer · downstream per-app orders (h5 page titles -> h4, per-level weight overrides) written after the release - 2026-10-04: Codex implemented (gpt-6-luna; probe of stale 2026-10-03 record succeeded, available) - orchestrator fixed review findings - review: codex/gpt-6-luna - lenses: envelope,regression,duplication,tests + ui_review - 3 raised - 2 accepted - worst accepted: defect (h4 mobile key now follows an app-overridden sm, test added; completeness test strengthened; rejected: breakpointDownQuery vs theme.breakpoints.down, no theme exists at token time) - light fixture diff limited to typography entries - no rendered side-by-side (no prototype; values asserted from the resolved theme in light and dark) - release 3.12.0 |
39
- | UCM-DEP-1 | All open Renovate updates in one change on main, one CI run | PRs #17 to #24 (npm non-major, actions, jsdom 30, pnpm 12, TypeScript 7, vitest 5) in one change; release only if runtime dependencies changed. Full order [work-orders/UCM-DEP-1.md](work-orders/UCM-DEP-1.md) | 2026-10-15 | planned | | Tier 3 (shared core, dependencies). Part of the PR close-out round of 2026-10-15 (operator decision 2026-10-15). Envelope by the Infra Expertenchat. Gate #1 approved by the operator 2026-10-15 (click in session local_1e5f212a-77db-4423-a507-983adf826bc4). |
40
- | UCM-I18N-5 | [User-list pagination range text is hard-coded English](work-orders/UCM-I18N-5.md) | `UserListComponent`'s `TablePagination` sets `labelRowsPerPage` but no `labelDisplayedRows`, so German shows "1–4 of 4". | 2026-10-01 | done | 6b9d557 | Found in jg-ferien's rendered check 2026-10-01. Follows `UCM-I18N-4`. Tier 3. Done = released AND jg-ferien pin bumped. Envelope written 2026-10-01; operator: hand to the jg Paket-4 session. Implementation map filled 2026-10-02 (`a732025`): the translation bundle carries FOUR locales (de/fr/en/sw), not three — confirmed by reading `authTranslations.ts` directly; proposed two keys (exact-count + "more than" form) with a best-effort, not native-verified, Swahili wording. Implemented by codex/gpt-5.6-luna exactly per the map: `labelDisplayedRows` resolves `UserList.DISPLAYED_ROWS`/`UserList.DISPLAYED_ROWS_MORE_THAN` through `t()`, both keys added in all four locales. review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui_reviewer (one concurrent batch, diff inline) · 4 raised · 3 accepted (deduplicated) · worst accepted: defect. Accepted: the rendered English test would still pass against a reverted implementation, since the EN wording was deliberately made identical to MUI's own hardcoded default — added a second test swapping in a distinctive marker string for just that key, proving the render genuinely resolves through i18next, not MUI's fallback; `authTranslations.test.js`'s new key-parity check only asserted non-empty strings, not that each locale actually carries the `{{from}}`/`{{to}}`/`{{count}}` placeholders the others do — strengthened with an explicit per-locale placeholder check; self-identified (corroborated by the `tests` lens's own closing remark) that the `count === -1` ("more than") branch can never be reached through a render of `UserListComponent` itself (its own `count` prop is always a real array length) — extracted the inline callback into an exported, directly unit-tested `formatDisplayedRowsLabel(t)` rather than leaving it permanently untested dead code. Rejected: `regression`'s claim that hard-coding `labelDisplayedRows` "overrides any consuming app's MUI theme customisation" — speculative, no actual consumer override exists (same explicit-prop design as `labelRowsPerPage` since `UCM-I18N-4`), and the reviewer's own repo search found zero consumer snapshots/assertions relying on the old English text. `envelope`'s "release + pin bump missing from the diff" was correct but not a code defect — addressed by the remaining steps below, not a fix to the reviewed diff. `duplication`: 0 findings. `ui_reviewer`: 0 blocking findings; confirmed placeholder consistency across all 8 strings; Swahili wording flagged as advisory-only ("structurally plausible to a non-native reviewer, native-speaker verification advisable" — not re-asked of a native speaker here, recorded as open). Tests: 37/37 green (`userScreenTranslations.test.jsx`, `authTranslations.test.js`, `UserListComponent.test.jsx`). Released as `3.11.1` (patch — continues `UCM-I18N-4`'s i18n-gap category, not a new capability area), commit `6b9d557`; npm Trusted Publishing confirmed via the Actions job log (not a registry re-query — provenance publishes can lag), tarball `@micha.bigler/ui-core-micha@3.11.1`. jg-ferien's pin bumped 3.7.0 -> 3.11.1 same day, closed as jg's own `JG-DEPS-3` (`2c1d6e3`) — see that repo's `WORK_ORDERS.md` for the empirical safety checks (status-colour palette diff, `UserListComponent`-unused confirmation, full-suite run) this multi-version jump required. |
41
- | UCM-TEST-1 | A kit integration check every app runs in its own suite | Exported `checkKitIntegration({ i18n, wrapper, pages })` -> `{ findings }` (shape of `assertThemeComplete`): kit keys present in the app's i18n, one `i18next` instance, kit pages render in the app's providers without raw keys, the `invalid`-email case shows the field text. Replaces per-app hand-written wiring tests and manual auth walks. Full order [`work-orders/UCM-TEST-1.md`](work-orders/UCM-TEST-1.md) | 2026-10-01 | done | 2cde120 | Tier 3 (shared-core, auth surface) · operator decision 2026-10-01: central check, apps adopt with their next bump · companion: `workflow-templates` WFT-CI-31. Codex implemented (available today, no fallback). Orchestrator independently found + fixed before review: a false-positive in the raw-code render heuristic (a legitimate short lowercase kit-shipped value, e.g. German `"oder"` for `Auth.LOGIN_OR`, would otherwise be flagged as a leaked backend code — added a `KIT_VALUES` exemption + a pinning test), and independently proved Required Test 5 (resolver-bypassed -> error-text finding) via a transient mutation of `resolveErrorText.js`, restored clean. Independent `reviewer` (all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 9 raised · 6 accepted · worst accepted: blocker**. Accepted and fixed: catalogue check didn't force `fallbackLng: false` per lookup, so a consumer's own `fallbackLng` config (survey_app's is `"de"`, not `"false"`) could silently resolve a missing-language key through the fallback and mask it (tests lens) — fixed, plus a dedicated test using a real fallback-configured instance; Required Test 6 only checked adapter reference-identity, not that a real post-check request actually reaches the real adapter (envelope lens) — strengthened with a sentinel-adapter test; React 19's `act()` needs `IS_REACT_ACT_ENVIRONMENT`, undocumented for consumers (ui lens) — added to the README example; README's `pages` description implied it fully replaces the default set, when `LoginPage` always additionally renders for the error-text check regardless (ui lens, nit) — clarified. Accepted, documented rather than redesigned: mutating the shared `apiClient.defaults.adapter` for the check's duration is process-wide and could intercept a concurrent real request from other code in the same test file (regression lens, blocker) — this is the Envelope's own decided stub design (`src/utils/auth-errors.js`-style module-singleton stubbing, same pattern already used throughout this suite), not fixable without abandoning that decision; added a README caveat instead of restructuring. Rejected: `RAW_CODE` only matches lowercase-snake-case, missing uppercase/hyphenated shapes (envelope + tests lenses, same point from both) — every real backend error code in this codebase (`src/utils/auth-errors.js`, allauth headless responses) is lowercase snake_case; widening the regex matches no real shape here and raises false-positive risk for marginal benefit. `KIT_VALUES` could let a leaked code evade detection if it coincidentally equals a real kit translation value (tests lens) — true but reintroducing the "oder" false positive to close it is a worse trade; no real backend code collides with the kit's UI copy in practice. The correctly-wired/legitimate-word/resolver/adapter-restore tests would all pass against a hardcoded `{findings:[]}` stub in isolation (tests lens, nit) — true of any happy-path assertion; each is paired with a failure-case test proving the same code path actually detects the defect (Required Tests 2-5), which is where the real proof lives. `duplication`: no capability-level overlap with `assertThemeComplete` or any shared-core/app module. **Verification** (WO's own instruction): ran `checkKitIntegration` once against survey_app's real wiring — not by touching that repo, but from inside this repo's own resolved environment, replicating `survey_app/frontend/i18n/index.js`'s real merge + config exactly (`fallbackLng: "de"`, survey_app's actual `translations.json`) plus this repo's real `AuthProvider` — **result: `findings: []`**, including against the real fallback-configured instance the tests-lens finding above was about. Orchestrator's own scoped run: `tests/checkKitIntegration.test.jsx` + `tests/resolveErrorText.test.js` (12 tests) + 7 directly-dependent files (33 tests) = 45/45 green via `npx vitest run <files> --pool=threads --maxWorkers=1`. No rendered gate — library-only change, no running app to preview. **Released as `3.10.0`** (minor — new exported capability, per the WO's own instruction). Per-app adoption rows are added by the Expertenchat when each app's next bump is scoped — not part of this WO. |
42
- | UCM-AUTH-11 | [`UserListComponent` offers an admin the delete action on their own row](work-orders/UCM-AUTH-11.md) | Without a `canDeleteUser` prop, `canDelete` falls back to `canEdit`, which is true for a superuser or role `admin` on EVERY row, their own included (`src/components/UserListComponent.jsx` `defaultCanEdit`/`canDelete`, ~l.128-156); the backend (dcm) still allows an admin's generic self-delete. Default should never offer deleting one's own row. | 2026-09-30 | released | 83bac37 | From django-core-micha `DCM-AUTH-4` (dropped there): operator decision 2026-09-30, UI-only, backend unchanged. Affected consumers measured: kerzenziehen and reimbursements render ucm's `AccountPage` without props (jg-ferien passes its own `userListCanDeleteUser` and already excludes itself). Both apps pin ucm **2.41.3**, so reaching them needs a major bump (3.0.0 removed chart layout props) — tracked as its own follow-up, not this WO. Gate #1 approved 2026-09-30 (operator handover). Codex implemented (available today, no fallback): new `isOwnRow(targetUser)` helper (`String(id) === String(id)`, `false` whenever either id is unresolved — covers the loading-state risk) gates the delete button's RENDER — `{showDeleteAction && !isOwnRow(row) && (...)}` — not its `disabled` state, so a consumer's `canDeleteUser` returning `true` for the own row cannot bring the button back; `canDelete`/`canEdit`/`defaultCanEdit` untouched. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 3 raised · 1 accepted · worst accepted: nit**. Accepted (nit, tests lens): the committed fixtures only ever used numeric ids for both `currentUser` and the own row, so a strict-equality implementation would have passed every committed test despite violating the Envelope's own `3`/`"3"` identity requirement — added a fifth case with a string-vs-number id mismatch. Rejected: envelope lens's claim that "Required Test 5 (mutation proof) is missing" — the WO's own Part B (mirroring `UCM-THEME-15`/`UCM-PRIM-2`'s established pattern in this repo) asks for a TRANSIENT demonstration during implementation, not a permanent fifth test case; the implementer's own `PROGRESS` narration confirms it removed the gate, watched tests 1 and 3 fail (2 delete buttons found instead of 1), then restored — exactly as asked. Envelope lens's claim that "package.json remains 3.9.0 so the release is incomplete" — true at the moment the reviewer read the diff (version bump + register are the Orchestrator's own finalize step, explicitly forbidden to the implementer), not a defect in the change itself; done now. `regression`/`duplication`/`ui`/`sec` lenses: no findings — `sec_reviewer` specifically confirmed the render-gate cannot be reopened via `canDeleteUser`/`canEditUser`/`extraRowActions`, and that the backend's own continued refusal of an admin's self-delete (`DCM-AUTH-4`) means this UI-only fix is not masking a still-open server-side gap. Orchestrator's own scoped run: `tests/UserListComponent.test.jsx` 7/7 green + `tests/userScreenTranslations.test.jsx` (directly-dependent, also renders this component) 4/4 green, via `npx vitest run <files> --pool=threads --maxWorkers=1` (default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue). No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.9.1`** (patch — behaviour fix of an existing component, no API change, per the WO's own instruction). `npm view @micha.bigler/ui-core-micha version` will read `3.9.1` once the registry's version-list index catches up to the just-completed publish (provenance-signed publishes report success immediately but can take a few minutes to appear in `npm view`/the registry API — confirmed via the GitHub Actions job log on the prior `UCM-AUTH-10` release, not re-verified here to avoid the same registry-lag false alarm). **Not done in this session** (per the WO's own Part C): registering the kerzenziehen/reimbursements major-bump follow-up rows in their own `WORK_ORDERS.md` registers — flagged for whoever next works in those repos. |
43
- | UCM-AUTH-10 | Error texts never show a raw backend code | One resolver for every kit error display: backend `i18nKey` > field key `Auth.field.<field>.<code>` > code key (AUTH-2) > backend message > caller default; `apiClient` sends the UI language as `Accept-Language`; `invalid`/`required` field keys for every submitted field; ~40 call sites migrated; reconstructs the missing 3.7.0 changelog entry. Full order [`work-orders/UCM-AUTH-10.md`](work-orders/UCM-AUTH-10.md) | 2026-09-30 | done | 9019d3d | Tier 3 (shared-core, auth screens, header on every request) · operator decision 2026-09-30: code-based UI, backend message as last specific fallback, UI language sent to the backend · found in `survey_app` SVA-DEP-1 acceptance (pre-existing, not a bump regression). Codex implemented (available today, no fallback): new `src/utils/resolveErrorText.js` (plain function, not a hook — called inside `catch` blocks) implementing steps 0-4 via `i18n.exists`/`i18n.t`; `extractErrorInfo`/`normaliseApiError` now carry `field` (`param` allauth / `field` dcm, normalised to one name), `i18nKey`, and `backendMessage` (never falls back to the code — the literal bug) as separate fields; `apiClient` gained a request interceptor sending `Accept-Language: i18next.language` only when `i18next.isInitialized` (never guesses); ~40 call sites across 22 files migrated to the uniform pattern (state now always holds resolved display text, never a bare key; render sites dropped their `t()` wrapper); 48 new `Auth.field.<field>.{invalid,required}` keys (24 fields, enumerated from `authApi.jsx`, all 4 locales + `de_informal` where wording differs); `CHANGELOG.md`'s missing `3.7.0` entry reconstructed. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 12 raised · 4 accepted · worst accepted: defect**. Accepted: (defect, envelope lens — a real guard blind spot) `MFAComponent.jsx`'s `setErrorKey('Auth.MFA_TOTP_ALREADY_ACTIVE')` was a plain literal-key setter the mechanical sweep missed — the call-site-guard test only matches the `.code` pattern, not "any literal setter needs `t()` now that render dropped its wrapper"; would have displayed the raw key on screen, exactly the bug this WO exists to fix. Fixed (wrapped in `t()`); no dedicated test exists for this component, fix verified by direct code inspection only. (nit, ui_reviewer) the new field-key catalogue omitted `RegistrationMethodsManager.jsx`'s 5 boolean policy-toggle fields (`allow_admin_invite`, `allow_self_signup_{access_code,open,email_domain,qr}`); added, all 4 locales. (nit, tests lens) `resolveErrorText.test.js`'s "dcm field shape" case was byte-identical to the allauth-shape case, testing nothing new (the param/field normalisation it claimed to cover already happens one layer down, in `extractErrorInfo`, and is genuinely tested there); removed the redundant duplicate. Rejected: 2 lenses (envelope, ui_reviewer) claimed `SupportRecoveryRequestsTab.jsx` calls `resolveErrorText(i18n, …)` without destructuring `i18n` — verified directly against the file twice, `const { t, i18n } = useTranslation()` is present at line 42 and every call site is a closure over it; a stale/misread finding from both lenses, not a real defect. 3 lenses (envelope, regression, tests) flagged step 3 (`backendMessage` rendered verbatim) as an exposure risk — this is the Envelope's own **decided** design (`Operator decision: … the backend message as the last specific fallback`), not an implementation defect; `sec_reviewer` — the lens specifically briefed with dcm's S4 audit-log rationale and tasked with exactly this question — found no HTML-injection path and confirmed React's text-node escaping covers every render site, which is what the Envelope's own risk-review request was actually asking. `sec_reviewer` additionally flagged `BulkInviteCsvTab.jsx:88`'s `err.message` and `PasswordInvitePage.jsx:117`'s `err.raw.messages` as chain bypasses — the former is a purely local CSV-file-read error with no backend response involved at all (confirmed no `apiClient` call in that `try` block), the latter is `UCM-AUTH-9`'s already-shipped, already-reviewed password-rejection-reason display, unchanged by this WO and out of scope per its own non-goal ("no change to which errors are shown, where, or what triggers one"). `envelope`+`tests` lenses' claim that `auth-errors.js`'s native `.message` still falls back to the code — deliberate, per this WO's own Part B ("you may leave `err.message` with its existing fallback-to-code behaviour untouched") and covered by the implementer's own test; the field the resolver actually reads (`backendMessage`) correctly never falls back, which is what the Envelope's separation requirement is about. Orchestrator's own scoped run: 14 files, 102 tests green (`resolveErrorText`/`loginPageErrorResolution`/`errorCallSiteGuard`/`apiClientLanguage`/`authErrors`/`AccountPage`/`PasswordInvitePage`/`SignUpPage`/`UserListComponent`/`QrSignupManager.registrationContext`/`i18nAggregate`/`authTranslations`/`themeCompleteness`/`i18nGermanVariant`), via `npx vitest run <files> --pool=threads --maxWorkers=1` (the default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue; the threads pool is stable). **Verification (the operator's exact screenshot case, rendered end to end):** `en` → "Enter a valid email address."; `de` → "Bitte geben Sie eine gültige E-Mail-Adresse ein." — both resolve via step 1 (`Auth.field.email.invalid`), never the raw `invalid` code, confirmed by `tests/loginPageErrorResolution.test.jsx` (`en`) and by the locale-agnostic resolver design (same key, `de` value) for `de`. No rendered browser gate — verified via the real `LoginPage` component rendered against a real i18next instance in the test harness. **Released as `3.9.0`** (minor, per the Envelope's own instruction — `resolveErrorText` is internal-only, not part of the public `src/index.js` surface). `npm view @micha.bigler/ui-core-micha version` should read `3.9.0` once `publish.yml` runs on this push. No consumer bump in this session (non-goal). |
44
- | UCM-THEME-15 | Derive status and control-border colours against the theme's own page | Baseline-supplied status `main` and `controlBorder` colours are derived against `#FFFFFF` + `background.default` (like `controlBorder.focus`), only when they would fail; a passing theme resolves byte-identically, app-set values are never touched. Full order [`work-orders/UCM-THEME-15.md`](work-orders/UCM-THEME-15.md) | 2026-09-29 | done | 4515e73 | Tier 3 (inside shared-core) · operator decision 2026-09-29: option (a), no exemptible contrast findings · unblocks `survey_app` SVA-THEME-1 and `survey_contact_app` SCA-THEME-1 (site half) after their pin bumps · lands before UCM-THEME-14. Gate #1 carried by the committed Envelope + mini-handover. Codex implemented (available today, no fallback): new `deriveDarkenedPageColour`/`deriveAlphaPageColour` helpers alongside (not replacing) the untouched `deriveFocusColour`/`clearsContrast`, using the kit's own alpha-aware `calculateContrastRatio` (not MUI's `getContrastRatio`, which is not documented to alpha-composite — matters for the translucent `controlBorder.main`/`.hover`); rule 3 (baseline vs app-supplied) checked per-key straight off `appConfig.palette`, never the resolved theme; `withMainShades` exported from `tokens.js` (internal-only, not part of the public package surface) so re-derivation reuses the real shade formula instead of duplicating it. Independent `reviewer` (all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff), `ui_reviewer` specifically traced rule 3 through every merge path (the three-arg `paletteTheme` build and the final layered `createTheme` call) including a partial `controlBorder` override (app sets `.hover` only) — confirmed no defect, per-key checks hold: **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 6 raised · 3 accepted · worst accepted: defect**. Accepted (defect, tests+envelope lenses, same finding): the app-override test only covered `success.main`/`controlBorder.main`, leaving `warning`/`error`/`info` and `controlBorder.hover`/`.error` unpinned — extended to cover a second overridden status + all three `controlBorder` states, plus asserted the two NOT-overridden status keys (`error`/`info`) still derive normally on the same tinted page (proves rule 3 is per-key, not per-object). (defect, tests lens) the termination test asserted only the status-derivation path terminates — added assertions that `controlBorder.main/hover/error` are also defined (not thrown) on the unreachable page. (nit, ui_reviewer) `DESIGN.md` said the baseline "promises a passing colour" — overstated, since capped derivation can still leave an unreachable page failing; softened to name the cap explicitly. Rejected: nit that exporting `withMainShades` violates "no API change" (it is an internal `tokens.js`↔`createAppTheme.js` module export, never re-exported from the public `src/index.js` surface — verified directly, zero consumer-visible surface change); nit that the byte-identity test "isn't evidence of the new derivation" (it is a different, explicitly required test — Required Test #2 — a regression/parity guard, not required to also prove the derivation path, which tests #1/#3/#4 already do). Orchestrator's own scoped run: `tests/createAppThemePageContrast.test.js` 9/9 green + 45/45 directly-dependent (`createAppTheme`/`themeCompleteness`), all via `npx vitest run <files> --maxWorkers=1`. No rendered gate — library component, no running app to preview; unit-tested only. **Verification data (re-derived from the built themes, per the Envelope's own acceptance criterion):** baseline `#FAFAFA` — unchanged (`success #1B8038`/`warning #976100`/`error #BF3227`/`info #01579B`/`border.main rgba(33,37,41,.50)`/`border.hover rgba(33,37,41,.65)`/`border.error #BF3227`). `hafen #D8E4E1`, `jg-fegbern #eceae2`, `routing #E8EAF6`, `uph #E6EAF0` (all four move the same two channels): `success rgb(24-25,115-121,50-53)`, `warning rgb(135-143,87-92,0)`, `border.main rgba(33,37,41,0.55)`; `error`/`info`/`border.hover`/`border.error` unchanged (matches the Envelope's table — only success/warning/border.main failed on these). `ygbs #E398C7` (all measured channels failed, all move): `success rgb(16,76,33)`, `warning rgb(90,58,0)`, `error rgb(124,32,25)`, `info rgb(0,65,116)`, `border.main rgba(33,37,41,0.6)`, `border.error rgb(171,45,35)`; `border.hover` unchanged (`.65` alpha already clears 3:1 even on this page). **Released as `3.8.0`** (minor: changed default for currently-failing themes only, no API change, no adopted app moves — per the Envelope's own framing). `npm view @micha.bigler/ui-core-micha version` should read `3.8.0` once `publish.yml` runs on this push. Next: add `planned` pin-bump rows in `survey_app/WORK_ORDERS.md` and `survey_contact_app/WORK_ORDERS.md` (from `2.41.3`, classify via the dependency-bump test in `AGENTS.md` → Tiering) and unblock `SVA-THEME-1`/`SCA-THEME-1` — not done in this session, flagged for the next one touching those repos. |
45
- | UCM-I18N-4 | [Formal/informal German per consumer; translate hard-coded English in user list and invite panel](work-orders/UCM-I18N-4.md) | Auth bundle gains an informal ("du") German variant chosen at registration time (default stays formal); `UserListComponent`, `UserInviteComponent` and `BulkInviteCsvTab` get keys for their hard-coded English. | 2026-09-29 | done | f4dfbe6 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-UX-10`. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4, last of the five, released together). Codex implemented (available today, no fallback): new `createUiCoreTranslations({germanVariant})` export (the existing `uiCoreTranslations` object stays untouched, byte-identical, for every consumer that doesn't opt in); 39 `authTranslations` entries whose `de` value matched a Sie/Ihr-form word-boundary scan got a sibling `de_informal` rewording (disclosed: machine-translated by the implementing model, not reviewed by a fluent German speaker, same disclosure standard as `I18N-3`'s Swahili backfill); 25 new keys for the hard-coded English already referenced via `t(key, englishFallback)` in `UserListComponent`/`UserInviteComponent`/`BulkInviteCsvTab`, plus one real code change (`labelRowsPerPage` wired on the `TablePagination`, the one string MUI supplies itself with no `t()` call at all). Independent `reviewer` (all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 9 raised · 2 accepted · worst accepted: defect**. Accepted: (defect, tests lens) the variant-switch test checked only one hardcoded representative key, so a factory stubbed to that one pair would have passed — added a second test iterating EVERY Sie-form key against its own `de_informal`/`de`, not one example. (defect, tests lens) the German-screen render test never exercised the empty-list (`UserList.NO_USERS`) or CSV-parse-error paths — added a case for both. Rejected: 3 lenses (envelope, regression, ui) claimed `UserList.DELETE_CONFIRM` "has no `de_informal`" — checked directly against the reviewed diff, the field was present (`de_informal: "Möchtest du diesen Benutzer wirklich löschen?"`); a stale/misread finding, not a real gap. "The parity test only checks Sie-regex-matched keys, not literal key-set parity" (4 lenses) — deliberate: a literal key-set-parity check is vacuously true here by construction (the factory falls back to formal `de` for any key with no override), so it would assert nothing; the Sie-form coverage test is what actually encodes the Risk note's failure mode (a formal key silently uncovered) and only formal-register German is linguistically incapable of avoiding the literal words "Sie"/"Ihr-", so the regex is a complete capture method for this bundle, not a narrow one. "`uiCoreTranslations` is no longer byte-identical, now carries `de_informal`" (regression lens) — accepted as a description, not as a defect: the `I18N-1`/`I18N-3` invariant this could threaten (`uiCoreTranslations[key]` structurally equals its source bundle's per-key value) is unaffected since BOTH sides now carry the same extra field consistently; every existing consumer's flatten-into-i18next-resources loop only reads named locale keys and silently ignores unknown ones (verified against `jg-ferien/frontend/i18n/index.js`'s actual merge loop). **Also found and fixed by the Orchestrator, not by a reviewer:** the implementer's `UserList.DELETE_CONFIRM` used informal "du" as its DEFAULT (`de`) value instead of formal "Sie" — the one new hardcoded-string key that actually needed register-dependent wording and got it backwards, breaking "default stays formal for existing consumers" for that one string; fixed the default to Sie-form and gave it a proper `de_informal` (now covered by the strengthened parity test). Orchestrator's own scoped run: 8/8 new (`i18nGermanVariant`/`userScreenTranslations`, incl. the two added cases) + 60/60 directly-dependent (`i18nAggregate`/`authTranslations`/`themeCompleteness`/`UserListComponent`), all via `npx vitest run <files> --maxWorkers=1`. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** (minor: two new public surfaces — `EmptyState` from `PRIM-2`, `createUiCoreTranslations` here — additive only, nothing removed); this lands all five WOs (`PRIM-2`/`SHELL-7`/`MSG-20`/`AUTH-9`/`I18N-4`) in one combined release per the operator's handover instruction. **`npm view @micha.bigler/ui-core-micha version` should read `3.7.0` once `publish.yml` runs on this push** — jg-ferien's pin bump (with the `germanVariant` option set) is that app's own separate step, not done here. |
46
- | UCM-AUTH-9 | [Auth flows that dead-end: rejected invite password hides the form, closed signup looks open](work-orders/UCM-AUTH-9.md) | `PasswordInvitePage` keeps the form after a server-side password rejection and shows the reason, confirms success; `SignUpPage` says when signup is off; `UserListComponent` role changes give feedback. | 2026-09-29 | done | fc28d74 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-UX-19` + the role-feedback part of `JG-UX-12`. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback): `PasswordInvitePage` split one shared `errorKey` into link-validity (`errorKey`, hides the form) vs submit rejection (`submitErrorKey`, keeps it, reads `err.raw.messages`); success now waits for an explicit "Go to login" click instead of an immediate `navigate()`; `SignUpPage` shows an info alert and no form when `signupModes.length === 0`; `UserListComponent`'s `handleChangeRole` now `alert()`s a success message on top of the pre-existing failure alert. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 9 raised · 4 accepted · worst accepted: blocker**. Accepted: (blocker, envelope+regression+sec, same finding from 3 lenses) `handleSubmit`'s catch treated EVERY `setNewPassword` failure as a retryable password rejection with no code check — a real race (token expires between the page's initial `verifyResetToken` check and this submit) would leave the form visible instead of the required invalid-link state; fixed by branching on `err.code === 'Auth.RESET_LINK_INVALID'` first (told apart by code, exactly what the Envelope's Risk note requires), routed to `errorKey` instead, new regression test added. (defect, envelope+regression, same finding) the closed-signup branch hid the pre-existing "already have an account / go to login" block too, turning the fix itself into a new dead end; moved that block outside the conditional so it always renders. (nit, duplication) the new `Auth.SIGNUP_CLOSED` key duplicated the existing, already-4-locale `Auth.SELF_SIGNUP_DISABLED`; reused it, removed the duplicate entry, updated the test's expected fallback text. Also fixed as a consequence: added a submit-time link-expiry regression test (`tests/PasswordInvitePage.test.jsx`) that would have failed against the original implementation. Rejected: tests-lens claim that the password-rejection fixture omitted `Auth.RESET_PASSWORD_INVALID` (it was already present in the first test); nit that the failure-feedback `UserListComponent` test "doesn't pin the new success behaviour" (it was never meant to — a separate test already covers the new success alert, this one guards the pre-existing failure path). Orchestrator's own scoped run: 17/17 (`PasswordInvitePage`/`SignUpPage`/`UserListComponent`, +1 new race-case test) + 58/58 directly-dependent (`i18nAggregate`/`themeCompleteness`/`authTranslations`), all via `npx vitest run <files> --maxWorkers=1`. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `PRIM-2`/`SHELL-7`/`MSG-20`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note; jg-ferien's pin bump is that app's own separate step. |
47
- | UCM-MSG-20 | [Message composer collapses on narrow screens](work-orders/UCM-MSG-20.md) | At 375 px with four actions the `Composer` textarea is 16 px wide and Send is clipped; plus linkified URLs (http/https only) and an explained or hidden read marker in `MessageBubble`. | 2026-09-29 | done | fcd4ae6 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-UX-14`. dcm register is canonical for `MSG-*`; this is the ucm-side row. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback): `Composer`'s action row split into an icon row + a message-field/send row (`sx={{flex:1,minWidth:120}}`/`sx={{flexShrink:0}}`); new `linkifyText.jsx` (strict `https?://` inclusion pattern, trims trailing sentence punctuation); `ReadTicks` wrapped in a `Tooltip` and hidden entirely when `recipient_count === 0`. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 6 raised · 3 accepted · worst accepted: defect** — the composer test only asserted CSS values, not that the actions actually moved to a separate DOM row (a same-row implementation with the same sx values would have passed); the read-marker/linkify test suite lacked a structural row-separation proof; and `linkifyText`'s raw `<a>` diverged from the kit's own `Link` primitive already used elsewhere (`CookieConsentStep.jsx`). Fixed by the Orchestrator: added a structural assertion (attach button's `Stack` ancestor does not contain the message field or send button); swapped `<a>` for MUI `Link` (which then broke `.closest('.MuiTypography-root')` in the punctuation test — Link itself carries that class — fixed to `.closest('p')`); **also found and fixed, not from a reviewer but from the Orchestrator's own test run:** the Composer's two new inline `sx` objects tripped `reportKitSxBypasses` (a pre-existing repo-wide guard neither the implementer nor any reviewer lens checked) — extracted to exported `COMPOSER_MESSAGE_FIELD_SX`/`COMPOSER_SEND_BUTTON_SX` with new `kitSxRegistry.js` entries. Rejected: "actions row split at every width, not just narrow" (Envelope's own wording allows "wrap to their own row" unconditionally, and a width-gated variant would need media queries for no stated benefit); the `Link`-vs-`<a>` and `minWidth: 120`-magic-number nits beyond what's already fixed above (the latter matches this repo's own `StatTile.jsx:10` `minWidth = 120` convention, not a violation); sec_reviewer found no issues (clean). Orchestrator's own scoped run: 14/14 new/extended (`messagingComposer`/`messagingLinkify`/`messagingReadTicksTooltip`) + `themeCompleteness` 38/38 (sx-registry re-check) + 16/16 directly-dependent regression (`messagingCompactBubble`/`messagingChunk6`/`messagingComposerChunk5`), all via `npx vitest run <files> --maxWorkers=1`. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `PRIM-2`/`SHELL-7`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note; jg-ferien's pin bump is that app's own separate step. |
48
- | UCM-SHELL-7 | [User-menu avatar initials fail contrast](work-orders/UCM-SHELL-7.md) | `UserMenu` avatar renders MUI default white-on-grey initials at 1.83:1; take colours from theme tokens with a passing fallback. | 2026-09-29 | done | 04a29fe | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. From jg `JG-DL-12`. Tier 3 (inside shared-core). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback): avatar background = `primary.main`, text dynamically picked between white/black by measured contrast (`calculateContrastRatio`, already exported). Independent `ui_reviewer` only (Codex gpt-5.6-luna) — Envelope scopes this to ui_reviewer alone, no new logic beyond a colour choice: **review: codex/gpt-5.6-luna · lenses: ui_reviewer · 1 raised · 1 accepted · worst accepted: defect** — the implementer's fallback used `ink.primary` (#212529, luminance ~0.018) instead of true black, which is NOT mathematically guaranteed to reach 4.5:1 together with `background.paper`: for a `primary.main` in the ~0.183–0.257 luminance band neither branch cleared 4.5:1, and neither required test exercised that fallback branch to catch it. Fixed by the Orchestrator: switched both branches to `theme.palette.common.white`/`.black` (true white/black, always present on any MUI theme, not a source literal) — contrast-against-true-white × contrast-against-true-black is a luminance-independent constant (21), so `max(...) >= sqrt(21) ~= 4.58` holds for every possible `primary.main`, closing the gap rather than narrowing it. Added a third test with a synthetic mid-grey `primary.main` (`#808080`, ~3.95:1 on white) to actually exercise the black-fallback branch. Orchestrator's own scoped run: `tests/UserMenu.test.jsx` 10/10 green (`npx vitest run tests/UserMenu.test.jsx --maxWorkers=1`). No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `PRIM-2`/`MSG-20`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note; jg-ferien's pin bump is that app's own separate step. |
49
- | UCM-PRIM-2 | [Shared EmptyState primitive](work-orders/UCM-PRIM-2.md) | New `EmptyState` export (icon, one line, optional secondary line and action) from theme tokens; jg-ferien shows empty sections in seven different ways today. | 2026-09-29 | done | 059e9f0 | Raised from jg-ferien's local browser test 2026-09-29; operator 2026-09-29: generic fixes go to ucm. Adopted in jg `JG-UX-9`. Tier 3 (new public export, additive). Gate #1 approved 2026-09-29 (operator handover covering UCM-PRIM-2/SHELL-7/MSG-20/AUTH-9/I18N-4). Codex implemented (available today, no fallback). Independent `reviewer` (Codex gpt-5.6-luna, all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna), concurrent background batch on the full diff: **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 5 raised · 3 accepted · worst accepted: defect** — missing README doc entry (Envelope required one, added), action button missing `type="button"` (form-submit risk, fixed), two test assertions passed for a would-be hardcoded stub (title/description always rendered the same fixed string across all 3 tests; strengthened with a second distinct-value render + negative assertion). Rejected: `sx` passthrough flagged as an "unrequested escape hatch" (established kit convention, e.g. `Composer`'s emoji button, not a new pattern); colour-guard/action-callback "could pass under a stub" claims (the callback test uses a distinct mock reference, which a hardcoded stub cannot satisfy; the colour guard test does exactly what Required Test #2 asked). Orchestrator's own scoped run: `tests/EmptyState.test.jsx` 4/4 green, `tests/themeCompleteness.test.js` (directly-dependent, directory-enumerated palette scan) 38/38 green. No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.7.0`** together with `SHELL-7`/`MSG-20`/`AUTH-9`/`I18N-4` in one combined release — see `I18N-4`'s row for the release note. |
50
- | UCM-THEME-16 | Dark-safe info box in `QrSignupManager` | `QrSignupManager.jsx:376` info box `grey.50` -> `background.subtle` (dark text was 2.00:1 on the fixed light ground). Operator decision 2026-10-02: the small light change `#FAFAFA` -> `#F4F5F6` is accepted; the QR code's own white ground (`:361`) and `ReadTicks.jsx` `grey.500` (6.07:1 in dark) stay unchanged on purpose. Full order [`work-orders/UCM-THEME-16.md`](work-orders/UCM-THEME-16.md) | 2026-10-02 | done | 7834231 | Tier 3 (change inside shared-core; corrected from Tier 2) · reviewer + ui_reviewer · patch release · raised by the UCM-THEME-14 review - 2026-10-02: Orchestrator implemented (one line, authorship with the Claude orchestrator; Codex not needed) - review: codex/gpt-5.6-luna - lenses: envelope,regression,duplication,tests + ui_review - 2 raised - 1 accepted - worst accepted: nit (hex in test assertion removed; rejected: apps without background.subtle, MFAComponent already depends on it) - release 3.11.2 - test fails on revert to grey.50 (mutation-checked) - no rendered side-by-side (no prototype; style assertion in both modes) |
51
- | UCM-THEME-14 | Dark mode in `createAppTheme`, from the central token file | `palette.mode: 'dark'` selects a dark baseline palette in `tokens.js`; focus derivation and completeness contrast checks judge a theme against its own surfaces instead of a fixed white; light output unchanged (regression guard). Full order [`work-orders/UCM-THEME-14.md`](work-orders/UCM-THEME-14.md) | 2026-09-29 | done | ddecdba | Tier 3 (inside shared-core) - Gate #1 carried by the committed Envelope + mini-handover - Codex implemented (gpt-5.6-luna, available today, no fallback); orchestrator fixed review findings (reverted two grey.* token swaps that changed light output, rgb() ink support, primary light/dark shades, test hardening, 0.02 alpha step to reproduce the frozen 0.38/0.53) - review: codex/gpt-5.6-luna - lenses: envelope,regression,duplication,tests + ui_review - 13 raised - 6 accepted - worst accepted: defect - light guard fixture proven against HEAD src - rendered check: dev harness Theme/Shared baseline, dark at 375 px and 1280 px (live browser screenshots; light not re-rendered, covered by the fixture guard) - full kit suite run once incidentally: 722 pass, 2 flaky under parallel load (messagingComposerChunk5, packageTreeShaking) pass in isolation - release 3.11.0 publishes on push - downstream: webshop-guenter pin bump planned
52
- | UCM-THEME-13 | Freeze a dark-mode baseline token set | Reference sheet `docs/DARK-TOKENS.md` with a decided value per mode-dependent token class plus a contrast table; `design-language` process, operator-led, no code. Full order [`work-orders/UCM-THEME-13.md`](work-orders/UCM-THEME-13.md) | 2026-09-29 | done | 7330f91 | Tier 2 (document only) · operator decision 2026-09-29: dark mode gets its own kit order · only consumer today: `webshop-guenter` · **Frozen 2026-10-02 by the operator** after two value rounds (neutral base A = estate convergence `#14181B`; surface step 1 = estate's own). Sheet `docs/DARK-TOKENS.md`, instrument `work-orders/assets/UCM-THEME-13-dark-prototype.html`. Invented and labelled: dark `info`, dark `stale`, overlay shadow, scrim. Document order: no implementer, no review (Envelope). |
53
- | AUTH-7 | `loginWithPassword`s 409-Retry gegen Zweitfehlschlag haerten | allauth-headless antwortet mit 409 ("already authenticated"), wenn die Session bereits gueltig ist; `loginWithPassword` ruft dann `fetchCurrentUser()` erneut auf. Schlaegt dieser Retry ebenfalls fehl, wurde der Fehler bisher roh durchgereicht statt normalisiert. Spec [`work-orders/AUTH-7.md`](work-orders/AUTH-7.md) | 2026-09-05 | done | ae7aa89 | **Tier 3** (Shared-Core). Ausgeloest durch `kira`: ein erschoepfter globaler Throttle-Bucket (siehe `kira/KIRA-TRACK-3`) liess den 409-Retry selbst mit 429 fehlschlagen, roh statt normalisiert durchgereicht. Rein additiver Bugfix, kein Prop-/Verhaltensvertrag geaendert. **Implementiert (Codex gpt-5.6-luna):** genau wie geplant, `src/auth/authApi.jsx` + neue `tests/authApiLogin.test.js`. review: codex/gpt-5.6-luna (`reviewer` 4 Lenses + `ui_reviewer` + `sec_reviewer`, ein Hintergrund-Batch) 3 raised · 1 accepted · 2 rejected · worst accepted: defect (aber ausserhalb des Scopes dieser WO, siehe unten) — `envelope`/`regression`/`duplication`/`sec_reviewer`: 0 raised. `tests`-Lens: 2 raised, BEIDE empirisch widerlegt (Fix testweise zurueckgerollt, betroffener Test schlug korrekt fehl — sind keine echten Luecken) → rejected. `ui_reviewer`: 1 raised, akzeptiert als real (empirisch bestaetigt: `normaliseApiError('...', 'Auth.LOGIN_FAILED')` liefert bei einem DRF-`{"detail": "..."}`-Fehler `code: 'GENERIC'` statt des übergebenen Default-Codes, weil `extractErrorInfo` in `src/utils/auth-errors.js` `'GENERIC'` bereits als gesetzten Wert zurückgibt, der `info.code || defaultCode` gewinnt) — **PRE-EXISTING in einer anderen Datei, nicht durch diesen Diff verursacht, an JEDER anderen Call-Site derselben Funktion bereits vorhanden** → out-of-scope fuer AUTH-7, nicht hier gefixt, nachverfolgt als `AUTH-8`. Tests: `npx vitest run tests/authApiLogin.test.js tests/AuthContext.test.jsx tests/authApiRegistration.test.js` 7/7 gruen. **PATCH-Bump 3.5.0 -> 3.5.1** (`package.json` + `CHANGELOG.md`), kein Consumer-Pin-Bump noetig (rein additiv). |
54
- | AUTH-8 | `extractErrorInfo`s `GENERIC`-Code ueberschreibt jeden `defaultCode` | Bei jedem DRF-Fehler mit `{"detail": "..."}`-Form (u.a. Django `Throttled`/`Http404`/`PermissionDenied` — genau die Form, die den urspruenglichen 429 aus `kira` ausloeste) liefert `extractErrorInfo` (`src/utils/auth-errors.js`) `code: 'GENERIC'`; `normaliseApiError`s `info.code \|\| defaultCode` waehlt dann IMMER `'GENERIC'`, nie den vom Aufrufer uebergebenen `defaultCode` (z. B. `'Auth.LOGIN_FAILED'`). `'GENERIC'` ist an keiner Stelle als i18n-Key registriert — die UI zeigt vermutlich den rohen String statt einer Uebersetzung. **Betrifft JEDE bestehende `normaliseApiError(...)`-Call-Site in diesem Repo**, nicht nur `loginWithPassword`. Spec [`work-orders/AUTH-8.md`](work-orders/AUTH-8.md) | 2026-09-05 | done | d3af6ac | Gefunden von `ui_reviewer` waehrend `AUTH-7`s Review, empirisch bestaetigt (`node --input-type=module -e "..."` gegen die echte Funktion). Operator-Entscheidung 2026-09-05: zuerst angehen. **Implementiert (Codex gpt-5.6-luna):** `code: 'GENERIC'` -> `code: null` im `data.detail`-Zweig, die anderen zwei Zweige unveraendert; neue `tests/authErrors.test.js` (4 Tests). review: codex/gpt-5.6-luna (`reviewer` 4 Lenses + `ui_reviewer` + `sec_reviewer`, ein Hintergrund-Batch) 1 raised · 1 accepted · worst accepted: defect — `envelope`/`regression`/`duplication`/`tests`/`sec_reviewer`: 0 raised (regression bestaetigte zusaetzlich per Grep: keine bestehende Test-Assertion haengt am alten `'GENERIC'`-Wert). `ui_reviewer` fand einen ECHTEN, empirisch bestaetigten Zusatzbefund: 7 von 39 im Code verwendeten `defaultCode`-Schluesseln (`Auth.RECOVERY_LOGIN_FAILED`, `Auth.USER_LIST_FAILED`, `Auth.USER_DELETE_FAILED`, `Auth.USER_ROLE_UPDATE_FAILED`, `Auth.USER_SUPPORT_UPDATE_FAILED`, `Auth.PASSKEY_CANCELLED`, `Auth.MFA_CHALLENGE_FAILED`) waren in KEINER der 4 Sprachen registriert — waeren also auch nach dem Code-Precedence-Fix weiterhin roh statt uebersetzt angezeigt worden. Auf Operator-Wunsch ("zuerst das angehen... stoert mich schon lange, auch dass die Meldungen nicht sauber uebersetzt sind") direkt mitgefixt: alle 7 Keys in `src/i18n/authTranslations.ts` ergaenzt (de/fr/en/sw), Stil an bestehende Nachbar-Keys angelehnt. Erneuter Scan aller 39 Aufrufstellen gegen die Uebersetzungsdatei: 0 fehlend. Tests: `npx vitest run tests/authErrors.test.js tests/authErrorCodeRendering.test.jsx tests/authApiLogin.test.js tests/authApiRegistration.test.js tests/authTranslations.test.js tests/i18nAggregate.test.js` 29/29 gruen. **MINOR-Bump 3.5.1 -> 3.6.0** (nicht PATCH) — aendert beobachtbares Verhalten fuer jeden `{detail}`-foermigen Fehler, keine rein additive Aenderung; Changelog warnt Konsumenten explizit, ihre Fehleranzeige nach dem Pin-Bump gegenzupruefen. |
55
- | MSG-19 | `DirectMessageLauncher` recipient picker: searchable Autocomplete replaces the flat List | Operator request: the new-DM recipient picker had no search, the same friction the fleet's own "Einfache Bedienung schlägt Vollständigkeit" rule already names ("a picker without search stops working long before the list feels long"). The `List`/`ListItemButton` picker is replaced with an MUI `Autocomplete` bound to the same `selectedId`/`selectedCandidate` state; the component's public prop contract (`candidates`, `scope`, `onOpen`) and its `start()`/scope-resolution logic are unchanged — only the picker's own rendering and selection mechanism changed. | 2026-09-05 | done | 7d99d82 | No WO file — operator-requested, fixed directly. Tier 3 (shared-core, `ui-core-micha`). Only current consumer of this component: jg-ferien (confirmed via a fleet-wide grep for `DirectMessageLauncher`), and jg-ferien's own tests fully mock it, so this change has zero blast radius there until jg-ferien separately bumps its `@micha.bigler/ui-core-micha` pin. Implemented directly in Claude (no Codex dispatch — pure component-level UI change). **review: claude/sonnet · 0 raised** (verified null-safety of `isOptionEqualToValue`/`getOptionLabel` against MUI's own `useAutocomplete` source, confirmed the rewritten test file preserves every one of the original 7 cases plus one new filtering case, confirmed i18n completeness) + **ui_reviewer: claude/sonnet · 4 raised · 4 accepted · worst accepted: defect** — (U1: the new "typed query matches nothing" state had no `noOptionsText` and would have shown MUI's hardcoded, untranslated English default — added `MessagingDirect.NO_MATCHES` in all 4 languages plus a test; U2: the Autocomplete was missing `fullWidth`, breaking from this repo's own one other Autocomplete-in-Dialog convention (`SupportRecoveryRequestsTab.jsx`) and regressing from the old List's full-width block layout — added; U3: reused `MessagingDirect.CANDIDATES`'s old plural-noun wording ("Recipients") as a visible single-select field label read awkwardly — reworded to an instruction-style label ("Select recipient") matching the sibling component's own convention, in all 4 languages; U4 (inferred, not rendered/verified): `autoFocus` on the TextField combined with the dialog's `fullScreen` mobile breakpoint could open the virtual keyboard unprompted — removed `autoFocus` entirely, since MUI's own `Dialog` already focuses the first focusable descendant on open by design, making it redundant). Two additional self-found nits fixed post-review: a defensive null-guard added to `candidateLabel` (matching the sibling `getUserOptionLabel` convention, even though confirmed unreachable under MUI's current calling contract) and a one-line update to `docs/messaging-deviations.md` item 18 (stale reference to "displays... candidate list" not mentioning the new search). Tests: `messagingDirectMessageLauncher.test.jsx` rewritten (9/9 green, up from 7 — the rewrite required discovering and documenting a real jsdom/RTL gotcha: MUI's `Autocomplete` only reacts to a typed value once the input is genuinely `.focus()`-ed, not merely `mouseDown`-ed) + `i18nAggregate.test.js` (4/4) both green; `tsc -p tsconfig.build.json` clean. Version bumped 3.4.0 → 3.5.0 (minor — additive UI capability, no prop-contract change) via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately, in its own repo, once this lands on npm. |
56
- | AUTH-6 | `QrSignupManager` additive `registrationContext`/`defaultMaxRedemptions` props | jg-ferien's `JG-MEM-1` (department QR-join) needs a per-department signup context and a high, poster-scale redemption default — `QrSignupManager` only exposed `enabled`/`expiryDays`. Two new optional props added, both defaulting to today's exact behaviour: `registrationContext` (merged into the generated token's `registration_context`) and `defaultMaxRedemptions` (overrides the low internal default). | 2026-08-28 | done | 1749c09 | No WO file — operator-requested, fixed directly, same convention as AUTH-3. Tier 3 (shared-core). Implemented by `codex exec` (jg-ferien's JG-MEM-1 dispatch touched this repo additively per that WO's own allowance), left uncommitted for the Orchestrator's own review before landing here. **review: claude/sonnet · 2 raised · 2 accepted · worst accepted: defect** — (R1: the new context-change effect also calls `setMaxRedemptions`, and with `maxRedemptions` in that same effect's dependency array, a context change after the admin had typed a custom redemption count fired a second, redundant `createSignupQr` call — fixed by reading the current value through a ref instead of a reactive dependency, so `setMaxRedemptions` no longer re-triggers the effect that just set it; R2: the one new test only covered the happy path on a fresh mount, with no test proving the existing `AccountPage` call (`enabled`/`expiryDays` only) stays byte-for-byte unaffected, and no test that would have caught R1 — two regression tests added: a bare old-prop-shape call asserting the exact legacy payload + single call, and a context-change-after-custom-redemptions call asserting exactly one `createSignupQr` invocation). Confirmed backward compatible against the one existing consumer, `AccountPage.jsx:369-372` (unaffected — it never passes the new props). Tests: `QrSignupManager.registrationContext.test.jsx` (3/3, incl. the two new regression cases) + `AccountPage.test.jsx` (4/4) + `SignUpPage.test.jsx` (10/10) all green — 17/17 total across the three files touching this component. No `ui_reviewer`/`sec_reviewer` (pure prop/effect-timing change, no visual surface, no auth-enforcement logic — the actual token validation stays entirely server-side in `django-core-micha`, already reviewed under `JG-MEM-1`). **Rebased onto a concurrent session's `UCM-CHART-17`/`-18` pushes (3.3.0/3.3.1 already published) — republished as 3.4.0 instead of the originally-committed 3.3.0** (minor — new capability, not a fix), via the repo's `publish.yml` (push-to-`main` + version bump). jg-ferien bumps its pin separately once this lands on npm. |
57
- | UCM-CHART-18 | The PNG export shows what the chart says, not what the user can click | Two defects, one file, a few lines apart -- both raised by `HRAM-CHT-7`'s independent `ui_reviewer` (2026-08-25) and confirmed, partly by reading and partly live. **(1) Interactive controls are in the image.** Since 3.3.0 the PNG rasterises `ChartFrame`'s whole `chartRef` box, and `chartRef` wraps `children` only -- not `toolbar`, `controls` or `meta` -- so anything a panel renders as children is now in the file: hram's `StructuralReachabilityPanel:468` footer `IconButton` (always visible, so **every** PNG of that panel carries a button), `AllocationPerformancePanel:536,548` and `OptimizationResultsPanel:1120,1141`. **(2) `transform` is not inlined** -- neither property list (`:32-36`, `:44-52`) has it, so `OptimizationResultsPanel:1107`'s rotated legend swatch exports as a **square instead of a diamond**. **Fixed here rather than in the app for two reasons:** it is a property of the element kind, not of each panel -- an exported image should show what the chart communicates, not what can be clicked -- and the alternative (moving controls into `ChartFrame`'s `controls`/`toolbar` slots) would **undo a deliberate design decision**, since `HRAM-RES-30` put that info affordance in the footer beside the caveat line. After this, hram needs **only a pin bump**. Full order [`work-orders/UCM-CHART-18.md`](work-orders/UCM-CHART-18.md) | 2026-08-25 | done | `5f34a2b` | **Tier 3** -- shared-core. `ui_reviewer` (what belongs in an exported image, and the MUI-legend counter-check) + `reviewer` (the ordering trap, the `transform` double-apply question); no `sec_reviewer`. **THE single most likely way to get this wrong:** `inlineComputedStyles` walks source and clone **in lockstep by index** (`:70-77`), so removing controls from the clone **before** it shifts every subsequent index and elements silently receive their neighbours' styles -- an export that looks plausible and is wrong everywhere after the first removal. Removal happens **after** the style walk, and the code comment must say so. **Counter-check that is not optional:** what disappears must be operation, not content -- a consumer whose legend items are `<button>`s (MUI's own legend can be interactive) would lose its legend, and the affected hram panels all pass `hideLegend` so they are **not** the test case; verify against a chart that keeps MUI's own legend, and if it vanishes that changes the approach. Also: `transform` may **double-apply** on SVG nodes that carry it as a presentation attribute the clone already keeps -- check `xLabels="angled"` before assuming it is inert. **Release: patch -> 3.3.1** -- this repairs behaviour 3.3.0 itself introduced rather than adding capability, the same reasoning `UCM-CHART-16` went out on, while `-15`/`-17` were minors because they changed what a consumer gets on purpose. Gate is **opening the files**, as for `UCM-CHART-17`: both defects were invisible to every test and visible immediately in an exported image. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly, no Codex dispatch. The lockstep-index ordering was respected from the start (`inlineComputedStyles` before `stripInteractiveControls`), `transform`/`transform-origin` added to the shared property list. **Verification (real Chrome, via the dev harness's module graph, no probe entry ever committed):** all four WO cases confirmed with hard evidence, not visual impression -- (1) a control in `children` (`IconButton` beside static text): captured the PNG path's actual intermediate SVG, confirmed no `<button` tag/text while the sibling and chart survive. (2) MUI's own legend (non-interactive, two series): fully intact, both labels present. (3) a rotated swatch: pixel-sampled the real rasterised PNG on a real `<canvas>` -- bounding-box corner reads background white, centre reads the swatch's own colour, i.e. a genuine diamond. (4) `xLabels="angled"`: the exported tick carries both the original `transform` XML attribute (kept by `cloneNode`) and the newly-inlined computed matrix; the matrix values mathematically match a SINGLE -45° rotation (`cos(-45°)=0.707107`), not a doubled one -- confirmed the inline `style` declaration wins over the presentation attribute rather than composing with it. 89 affected chart tests green (14 in `exportChart.test.js`), `tsc` clean. **Review: codex/gpt-5.6-luna (`reviewer` + `ui_reviewer`, concurrent) · 5 raised · 3 accepted · worst accepted: high (interactive-legend content loss)** (id not divisible by 5, no rotation configured). **Both reviewers independently converged on the SAME defect**, exactly the risk the WO's own Envelope named and required a live counter-check for: the initial fix REMOVED (not unwrapped) MUI's own interactive legend items (`onItemClick`/`toggleVisibilityOnClick` renders each item as `<button role="button" class="...MuiChartsLegend-series...">`), deleting the swatch+label along with the click handler -- the WO's own words, "if it vanishes, that is a finding that changes the approach, not a detail to paper over." FIXED: `MuiChartsLegend-series` matches are now UNWRAPPED (replaced with a plain `<span>` carrying the same class/style/children) instead of removed; re-verified live against an ACTUAL interactive legend (`slotProps={{legend:{toggleVisibilityOnClick:true}}}`, confirmed the live items render as real `<BUTTON>` tags) -- export contains zero `<button` tags, `MuiChartsLegend-series` and both series labels survive intact. `reviewer` additionally found: the interactive-control selector missed `a[href]`/`[role="link"]` (accepted, added -- also corrects the CHANGELOG's own "drill-down links" claim, which wasn't true until this fix); and the original ordering test's button was the LAST descendant, so a wrong call order wouldn't have shifted any node the test actually asserted on -- content-presence checks can't observe a style misattribution, only a wrong STYLE VALUE can (accepted, rewrote with a distinctively-styled trailing sibling, confirmed by temporarily reverting the call order that the new assertion genuinely fails). Two findings rejected as decoration/informational only (transform double-apply already reasoned through correctly by both reviewers; a jsdom-literal vs. browser-matrix distinction in the unit test, already covered by the separate real-browser verification track). |
58
- | UCM-CHART-17 | The two export buttons make two different promises; make them keep them | **Export SVG** yields the chart as scalable, correctly styled vector art; **Export PNG** yields what the user is looking at -- legend, size key and footnotes included. **Defect, from source:** `exportChart.js:7-11` clones the first `<svg>` and serialises it raw, with **no styles carried**. MUI X-Charts styles nearly everything through emotion CSS classes (tick fonts/sizes, axis and grid colours, text fill, theme typography); the clone keeps `class="MuiChartsAxis-tickLabel css-1abc"` and none of the rules behind it, so a standalone viewer applies bare SVG defaults -- that is the operator's "graphically massively different", and **the PNG inherits it** because it rasterises the same blob. **Second cause, and it is ours:** `findSvg` takes the *first* `<svg>`, i.e. the chart, while since `HRAM-RES-39`/`HRAM-VIS-2` consuming panels hide MUI's legend and render a hand-built one as **HTML** beside the chart with the size key as a *second* SVG -- both outside the exported element. **Before that consolidation MUI's legend was inside the chart SVG and did get exported**; fixing a legend overlap silently degraded the export, and nobody noticed because nobody opened an exported file. Third: "the first SVG" is fragile regardless -- it works today by accident of ordering. **Operator chose option C (2026-08-25):** SVG keeps clone-and-serialise but **inlines computed styles**, staying true vector and chart-only; PNG rasterises the whole `chartRef` container so the output matches the screen by construction. Full order [`work-orders/UCM-CHART-17.md`](work-orders/UCM-CHART-17.md) | 2026-08-25 | done | `fa81181` | **Superseding an earlier "in-progress, not committed" note left by a concurrent session on this same WO** (per AGENTS.md's Parallel agent sessions: allowed, no pause needed -- their implementation stayed local/uncommitted, this row now reflects the one that actually landed). **Dependency question resolved, no approval-gate trigger needed:** no rasteriser added. Chrome taints the canvas on `toBlob` unconditionally once the source SVG carries a `<foreignObject>` with HTML content -- confirmed live with a minimal repro (zero external references, still throws `SecurityError`) -- but **only when loaded via a `blob:` object URL**; the identical SVG loaded via a `data:` URI does not taint at all. That swap (`rasterize()` in `exportChart.js`) is what makes the platform-only approach genuinely viable, not merely attempted; re-verified end-to-end against the real, unmocked `exportChartPng`. Also checked per the risk section's own instruction: the PRE-FIX path (no `foreignObject`) was never tainted -- the exposure is specific to the `foreignObject` this WO's PNG path adds. **Verification (real Chrome, via the dev harness's module graph -- no probe entry ever committed to `dev/entries.jsx`):** a panel shape mirroring `AccessGapScatterPanel` (hand-built HTML legend + a second "size key" SVG beside the chart) exported to PNG at 2x scale with no taint; pixel-sampling the produced PNG at the legend swatch's and size-key's known screen coordinates read back their EXACT source colours (`#1976d2`/`#9c27b0`) -- the legend and size key are genuinely present in the raster, not just "no error thrown". SVG export of the same panel: contains the chart (`MuiChartsSurface-root`), correctly excludes the legend. A plain no-legend panel shape also verified clean on both paths. 192 affected chart tests green, `tsc` clean. **Review: codex/gpt-5.6-luna (`reviewer`) · 3 raised · 2 accepted · worst accepted: high (SVG intrinsic sizing).** (1) ACCEPTED, high -- the exported SVG carried only a `viewBox`, no explicit `width`/`height` attributes (MUI's `ChartsSurface` sizes itself via CSS `100%`, which resolves to nothing once cloned standalone; a viewer would fall back to the 300x150 SVG default) -- fixed by setting both from the live element's `getBoundingClientRect()`, re-verified live (`width="466" height="400"` on a real export). (2) REJECTED, factually incorrect -- claimed jsdom lacks `URL.createObjectURL`/`revokeObjectURL` so the new tests "will fail before their assertions"; disproven directly, all 8 (now 9) tests in `tests/exportChart.test.js` pass, and jsdom 29.1.1 implements both natively (verified). (3) ACCEPTED, medium -- the curated style-property lists were missing `overflow` and CSS Grid properties (`grid-template-columns/rows/areas`, `grid-area`, `justify-items`, `align-content`) relevant to MUI's own composed layouts (`ChartsSurface` itself sits in a `grid-area`), not only the hand-built-flex-legend shape this WO's own verification exercised -- added. **`ui_reviewer`: codex/gpt-5.6-luna · 2 raised · 2 accepted · worst accepted: nit** (this dispatch explicitly instructed "review only, no file/test mutation" after the prior WO's runaway codex `ui_reviewer` -- ran clean, focused, produced a verdict, no repeat of that failure mode). `describeChild` confirmed correct (preserves the button's accessible name, uses `aria-describedby` rather than replacing it via `aria-label`), applied to both buttons, export-UI structure otherwise unchanged, all four locales present. Findings, both accepted: (1) the PNG tooltip named the legend but not the size key/footnotes the format also carries -- reworded across all four locales to match the Envelope's own "legend, size key and footnotes" wording. (2) the Swahili SVG-tooltip word for "legend" (`kigezo`) actually means criterion/parameter, not legend -- corrected to `maelezo` in both SVG and PNG Swahili tooltips. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.3.0 > 3.2.1`). Consumer adoption (hram's pin bump) is a separate row -- a shared-core fix published but not consumed is not finished.
59
- | UCM-CHART-16 | The x-axis band starves MUI's tick-fit check, and two consumers have patched around it | A chart with an axis title **and** horizontal tick labels must render its tick labels without a consumer hand-setting `xAxis[].height`. **Defect:** on the horizontal path `resolveXAxisGeometry` reserves one combined band from `TICK_BAND_BASE_PX` + `AXIS_TITLE_BAND_PX`; once a title is also taken from that budget, MUI's own fit check (`shortenLabels` against `tickLabelsMaxHeight`) is left **just under one line of 12 px text** and blanks **every** label regardless of content. Distinctive and easy to misread: correctly positioned tick marks, zero non-empty label texts -- looks like a data or formatter problem and is neither. **Evidence: two consumers, same workaround, same magic number**, independently, in two different WOs -- hram `StructuralReachabilityPanel.jsx:386` (`HRAM-RES-32`) and `AccessGapScatterPanel.jsx:212` (`HRAM-RES-39`), both `height: 56`. RES-32's own comment already diagnosed it and called it *"a pre-existing, latent shared-core near-miss"*, then patched locally because a shared-core fix was outside that WO; RES-39 read the MUI source, reached the same cause and the same 56, and patched locally again. **The third titled-horizontal-axis chart will hit it too**, and the package is meanwhile violating its own stated Rule 1 (*"nothing outside the resolver does chart arithmetic"*) in two places. Fix: correct the reservation, and **derive** it from the known tick font size and title band rather than adding a second magic number that will be wrong at a different font size. Full order [`work-orders/UCM-CHART-16.md`](work-orders/UCM-CHART-16.md) | 2026-08-25 | done | `9830266` | **Tier 3** -- shared-core. `reviewer` + `ui_reviewer` (the plot-area change across consumers); no `sec_reviewer`. **The risk that decides this WO: a mocked prop-assertion test cannot catch it.** The file already records the same trap for a neighbouring bug (*"a mocked prop-assertion test cannot catch this, it only reproduces on an actual MUI scale computation"*) -- a test asserting the computed band's number passes whatever the number is. **Test 1 must observe rendered label text**, or it is decoration. Also: raising the band changes every titled-horizontal-axis chart's plot area, on top of `UCM-CHART-15`'s 80 px shift -- and fixing it too generously wastes vertical space everywhere, so the near-miss margin is the budget, not a number rounded up to be safe. Release patch unless the plot-area change warrants minor; say which and why in the CHANGELOG. Consumer cleanup (removing hram's two workarounds) is `HRAM-CHT-6`, after this ships. **Third occurrence, 2026-08-25, and it changes the fix:** hram's `AccessibilityPanel` in `Urban/Peri-urban/Rural x Division` mode -- five band categories, five rendered bar groups, **no tick labels at all**, same signature. But that chart has **no `xAxisLabel`**, so the title is not competing for the band; what it has is `tickLabelStyle: { fontSize: 13 }`, one pixel above the 12 px the base band was sized against. **So the trigger is not "a title takes the room" -- it is that the band is a near-miss at all**, and anything consuming a little more pushes it over. A fixed `height: 56` patched into a third consumer would work at 13 px and fail at 14. That is exactly why scope item 2 requires the resolver to **derive** the requirement from the tick font size it already knows, instead of adopting the constant two consumers measured empirically at 12 px. Signature-matched, not yet instrumented -- confirm the mechanism on this third case, since it is the one that distinguishes a derived fix from a bigger constant. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly by the Orchestrator, no Codex dispatch. **The defect could not be confirmed from source reading alone** (jsdom does not implement real SVG text measurement, `getBBox`, so a rendered test would trivially pass regardless of correctness): reproduced and calibrated against the real, unmocked `BarChart` in this repo's own dev harness (Chrome, DM Sans, via `dev/entries.jsx`, reverted before commit) -- confirmed the defect reproduces exactly as described, confirmed it does NOT reproduce under MUI's default Roboto stack (explains why it was font-dependent and missed until a consumer used a taller font), then binary-searched the real minimal `xAxis[].height` at ten font-size/title combinations (11-18px) to derive `TICK_TEXT_HEIGHT_FACTOR=1.3`, `TICK_BAND_MUI_OVERHEAD_PX=9`, `TICK_BAND_TITLE_EXTRA_PX=2` -- matched every measured threshold within 1px, never under. Third occurrence (no title, `tickLabelStyle.fontSize:13`) confirmed via the same harness. 180 affected chart tests green (78 `chartDefaults.test.js` + 2 new rendered tests in `chartAxisTickBand.test.jsx`, which install a calibrated `SVGElement.prototype.getBBox` mock since jsdom's real implementation always measures 0x0 + 100 across `BarChart`/`LineChart`/`ScatterChart`/`TimeSeriesChart`/`ChartFrame`), `tsc -p tsconfig.build.json` clean. Both new resolver tests and both new rendered tests independently confirmed to fail against the pre-fix resolver (temporarily reverted, reran, restored) -- test 1 must observe rendered label text, not a computed number, per the WO's own risk section; both requirements met. Version bumped `3.2.0` -> `3.2.1` (patch, per the WO's own classification -- repairs existing behaviour, `UCM-CHART-15`'s heights untouched). **Review: codex/gpt-5.6-luna (`reviewer`) · 3 raised · 1 accepted · worst accepted: defect · chosen** (id not divisible by 5, no rotation configured). `reviewer` findings: (1) ACCEPTED -- the Rule-2 guardrail test only asserted `xTitleBand === 0`, which passes whether or not the title margin leaked into `xAxisBand` instead; fixed to assert the actual band/height values too. (2) REJECTED, out of scope -- a caller-supplied `xAxis[].tickSize` override isn't a documented/supported customization point of this package (grep confirms zero call sites, in this repo or its own tests/docs, ever set it); adding speculative handling for an unused knob is the "second magic number" pattern this WO exists to avoid. (3) REJECTED, out of scope -- a custom/multiline title font size overflowing the fixed `AXIS_TITLE_BAND_PX=20` reservation is a pre-existing `UCM-CHART-12`-era limitation, unchanged and unworsened by this WO. **`ui_reviewer`: codex/gpt-5.6-luna dispatch produced NO USABLE VERDICT** -- ran for an extended period repeatedly re-executing `vitest`/`git` against the live working tree (including applying then reverting a debug `console.log` in the new test file -- confirmed harmless via `git diff`/content check, but a review process should never write to the shared tree) and terminated with exit 0 but no findings/verdict text. Treated as unusable per "a review never lapses because a provider is down" -- fell back to `review_fallback` (`claude`/`sonnet`), spawned read-only, explicitly instructed not to run tests or mutate anything: **no findings**, confirmed the fix is byte-identical to the pre-fix band at the package's own default tick font (zero growth for unaffected charts), the CHANGELOG communicates the visual consequence plainly, and patch is the right classification. **Flagged for awareness, not fixed here:** the codex `ui_reviewer` runaway-without-verdict pattern is worth watching for future Tier-3 dispatches -- consider adding an explicit "review only, do not modify files or run mutating commands" line to review prompts. |
60
- | UCM-CHART-15 | Shift the chart size scale up one step, and raise the ceiling | `compact` 30->**40** (240->**320 px**), `standard` 40->**50** (320->**400**), `tall` 50->**60** (400->**480**), plus **`extra_tall` 70 (560)** and **`super_tall` 80 (640)**. The 10-unit step is preserved and every value stays a clean multiple of the 8 px spacing unit. **Two pressures, both from real call sites:** the ceiling is too low -- hram already uses the documented `height` escape to get past `tall`, with a comment recording exactly that ("no size token goes taller than `tall`") -- and the whole scale is too cramped, which lands on a value that was never chosen on its merits: `chartDefaults.js:295` says `standard` is "pinned to the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this migration does not also silently redraw every already-shipped default-sized chart". **That was a migration guardrail, not a design verdict** -- the migration is complete, so what remains is a height inherited rather than decided. Full order [`work-orders/UCM-CHART-15.md`](work-orders/UCM-CHART-15.md) | 2026-08-24 | done | `be2eb6b` | **Tier 3** -- shared-core, by the tiering table regardless of diff size. `reviewer` (enumeration completeness, and whether the test can actually fail) + `ui_reviewer` (does a 10-unit step still read as a coherent scale at five members, is `super_tall` distinguishable from `extra_tall`); no `sec_reviewer`. **Blast radius is one app, and that is the argument for doing it now:** measured 2026-08-24, hram is on **3.0.1 and is the only 3.x consumer**; the other 14 apps are on 2.41.3/2.37.0, where the size tokens do not exist (a 3.0.0 feature, this repo's CHANGELOG marks 3.0.0 "Breaking"). Fourteen apps have yet to migrate, and every one that migrates onto a scale already known to be too cramped must be revisited afterwards. **Version: minor (3.2.0) by operator decision on 2026-08-24, against the Expertenchat's recommendation of major** -- redefining what an existing public token renders is breaking under semver. Recorded because the consequence is real: a minor can arrive through Renovate's weekly run without a human reading it as a visual change, and on this estate that triage is also what opens `develop -> main`. **Two obligations follow and are release gates, not decoration:** the CHANGELOG entry must **lead** with "every existing chart becomes 80 px taller", and the consuming pin bump is coordinated (`HRAM-CHT-5`), never left to the weekly bump. **Most likely defect: a half-updated enumeration** -- the valid-size list is written out in the resolver's error message, `assertRemovedChartProp` in three chart components, three JSDoc blocks and `docs/CHART-LAYOUT.md`; grep the literal `"tall"` before declaring it done. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly by the Orchestrator, no Codex dispatch. Grep for the literal `"tall"` confirmed the WO's own enumeration-site list was complete, nothing extra found. 176 affected chart tests green (76 `chartDefaults.test.js` including two new tests hardcoding all five pixel values independently of `CHART_SIZE_SPACING_UNITS`, so they fail against the pre-CHART-15 scale + 100 across `BarChart`/`LineChart`/`ScatterChart`/`TimeSeriesChart`/`ChartFrame`), `tsc -p tsconfig.build.json` clean. Version bumped `3.1.2` -> `3.2.0` (minor, per the WO's operator decision), CHANGELOG entry leads with the redraw. **Review: codex/gpt-5.6-luna (`reviewer` + `ui_reviewer`, concurrent, diff inline) · 2 raised · 2 accepted · worst accepted: nit · chosen** (id not divisible by 5, no rotation). `reviewer`: nit -- a stale `"240px"` example in a `theme.spacing` comment at `chartDefaults.js:416`, unrelated to runtime behaviour (fixed to `"320px"`); confirmed all five tokens consistently enumerated everywhere and that the new tests fail against the old scale. `ui_reviewer`: nit -- the CHANGELOG lead didn't explicitly say "breaking" or name the new `standard` default for a Renovate skimmer (fixed, lead now reads "Breaking visual change: every existing chart using a `size` token -- including the default `standard` size -- becomes 80px taller; `standard` is now 400px"); judged the five-member scale coherent and `extra_tall`/`super_tall` distinguishable. No further reviewer pass run on the two nit fixes (in-scope, no approval needed). |
61
- | DX-3 | `publish.yml` tests before it builds, so `2.41.2` cannot publish | `DX-2` landed on `main` (`31675d8`, `e75192f`) and **its publish failed** — run [32029216699](https://github.com/bigler-webapps/ui-core-micha/actions/runs/32029216699), `completed/failure`. `publish.yml` runs `Run tests` **before** `Build`; `DX-2`'s new `tests/packageTreeShaking.test.js` copies the package's own `dist/` to assemble a fake installed consumer, and **`dist/` is gitignored** (`.gitignore:2`), so in a clean checkout it does not exist yet: `ENOENT ... lstat '.../dist'`, 510/511 passing with the one new test as the sole failure. The test is neither flaky nor wrong — it inspects the built artifact, which is the point; it cannot pass in the order the workflow runs. It was green locally at 511/511 only because a `dist/` from an earlier build was lying around. Full order [`work-orders/DX-3.md`](work-orders/DX-3.md) | 2026-08-17 | done | 67fe140 | Tier 3 — CI surface, independent `reviewer` mandatory for two lines; no `ui_reviewer`. Fix: move `Build` above `Run tests`, **keeping `if: steps.version_check.outputs.should_publish == 'true'` on both** — dropping one is the only way a reorder can go wrong, so the reviewer is asked to confirm it explicitly. Codex implemented (available today per `.claude/codex-status.md`, no prior fallback needed) — one-file, 4-line diff, nothing else touched. **Independent `reviewer` (Sonnet) confirmed: only file changed, both `if:` conditions retained on Build and Run-tests, order now install→Build→Run-tests→npm-bump→Publish, `tsc` (Build) emits `dist/` before `vitest run` (tests) needs it — no findings, safe to commit.** **Dispatch:** landing the fix did not re-trigger `publish.yml` (its `paths` filter excludes `.github/workflows/**`) — operator confirmed on request, `gh workflow run publish.yml --ref main` dispatched, run [32032549303](https://github.com/bigler-webapps/ui-core-micha/actions/runs/32032549303) `completed/success`, `tests/packageTreeShaking.test.js` passing (not skipped) in the run order fixed above. **Acceptance verified against the registry, not the tile: `npm view @micha.bigler/ui-core-micha version` → `2.41.2`.** `DX-2`'s deliverable is now actually consumable. **The dispatch is the part most likely to be missed:** `publish.yml`'s trigger `paths` cover `package.json`/`pnpm-lock.yaml`/`src/**`/`tsconfig.build.json` but **not `.github/workflows/**`**, so landing this fix does NOT re-trigger the publish — `2.41.2` sits unpublished until `publish.yml` is dispatched on `main`, which needs explicit operator confirmation (not on the safe-allowlist). No version bump: the artifact being published is DX-2's. **Acceptance is `npm view` returning `2.41.2`, not a green tile** — the publish status of this package has now been misreported in both directions in one session. Do NOT skip, delete or self-build the tree-shaking test; a skip would silently retire the only check that `sideEffects` actually tree-shakes. **Recorded, out of scope:** `publish.yml` is the ONLY workflow here that runs tests, and only when `should_publish` is true — so ucm's suite runs in CI *only on a version bump*, and a `src/**` change without one is never tested. Real gap for a twelve-consumer shared core, separate decision. |
62
- | DX-2 | Declare `sideEffects` so consumers can tree-shake the kit | `package.json` has **no `sideEffects` field** and no `exports` map, so a bundler must assume every module can have an import side effect and cannot drop unused exports — twelve consumers ship the whole kit whatever they import. Measured, not theorised: `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**. Full order [`work-orders/DX-2.md`](work-orders/DX-2.md) | 2026-08-12 | done | 31675d8 | Tier 3. Codex implemented (available today, no prior fallback needed). **Array form used, not blanket `false`** — the per-module audit found exactly one real import-time side effect group: font CSS. `src/theme/createAppTheme.js` and `src/theme/index.js` both used to `import '@fontsource/dm-sans/*.css'` at module scope; isolated into a new `src/theme/fonts.js`, declared in the `sideEffects` array (both `src/`/`dist/` forms) alongside `index.js`, `theme/index.js`, `theme/createAppTheme.js`, and `notifications/serviceWorker/sw.js` (a genuine `addEventListener` side effect, conservatively listed even though it isn't reachable from `src/index.js` — consumers import it directly). `uiCoreTranslations`'s aggregate-object construction relocated verbatim from `src/index.js` into `src/i18n/uiCoreTranslations.js` (no shape/value change) so the entry module carries no non-trivial logic beyond its own font import. New test `tests/packageTreeShaking.test.js` builds two throwaway consumer bundles against the real `dist/` output via Vite and asserts translations actually tree-shake. **Independent `reviewer` (Sonnet) redid the module-by-module audit from scratch** (walked theme/, auth/apiClient, notifications/realtime, messaging/, onboarding/, i18n/ for CSS imports, `document`/`window`/`self` mutation, axios interceptors, WebSocket construction, i18next `.use()/.init()`) and independently ran `pnpm run build` + the new test — **verdict: classification correct and complete, no missed side effect, safe to commit.** One non-blocking finding, addressed: **`src/index.js`'s font import is unconditional, so `@fontsource/dm-sans` CSS ships in every consumer's bundle regardless of import — it does NOT tree-shake**, and the reviewer flagged that the reported byte delta could be misread as covering the whole entry when it is translations-only. Added a one-line comment at `src/index.js:3` documenting the font/`sideEffects`-array coupling so a future edit can't decouple them silently (the exact false-negative hazard this WO exists to prevent). **Re-measured delta** (built `dist/`, minified, real bundler, supersedes both `I18N-1`'s `+28,943 B`/`+7,327 B` source-reachability figure and `THEME-2`'s `−11 bytes` no-tree-shaking baseline): auth-only **62,294 B raw / 17,653 B gzip** vs. aggregate **90,395 B raw / 25,045 B gzip** → **+28,101 B raw / +7,392 B gzip, translations only — font CSS is a separate, always-shipped, non-tree-shakeable cost not included in this number.** Written into `I18N-2`'s note below. Scoped tests: `pnpm run build` green, new test green, **full existing suite 511/511 green** (WO called for the existing suite as the scoped run, not full-estate). No rendered gate (nothing renders). Version bumped to `2.41.2` (patch by scope, per the WO). **Correction (2026-08-17): the publish failed** — `publish.yml` runs tests before build, and this WO's own new tree-shaking test needs `dist/` (gitignored), so it cannot pass on a clean checkout; registry stayed at `2.41.1`. Fix + re-publish tracked in `DX-3`. |
63
- | I18N-3 | Backfill Swahili in `messagingTranslations` and restore the four-locale shape test | `messagingTranslations.ts` carries **116 keys with `de`/`fr`/`en` and zero `sw`**, while all six other bundles carry all four locales. `I18N-1` hit this writing its aggregate test, confirmed the gap as pre-existing, and the operator scoped the assertion down to `{de,fr,en}` with the backfill recorded as a follow-up — **which was never written**. Full order [`work-orders/I18N-3.md`](work-orders/I18N-3.md) | 2026-08-12 | done | 30229d6 | Tier 3 — re-tightens a deliberately loosened test, so every future bundle must then supply `sw` or fail CI. **No app is broken today**: `sw` is configured only in hram and hram does not register messaging — this closes an inconsistency and re-arms a test, which is why there is no urgency justifying unreviewed strings. Codex implemented the two named files (`messagingTranslations.ts` backfill, `i18nAggregate.test.js` restored to `{de,en,fr,sw}` + stale scope-down comment removed) and correctly **stopped rather than exceed its named 2-file scope** when it found `tests/messagingMsg6.test.jsx:103` had a pre-existing exact `toEqual` hardcoding the old 3-locale shape for `MessagingThread.SHOW_REPLIES_one`, which the additive `sw` property broke. **Orchestrator fixed that one line itself** (added the matching `sw` value to the assertion) as necessary mechanical collateral of the in-scope change, not scope creep. **Translation source: machine-translated by the implementing Codex/GPT model, NOT reviewed by a fluent Swahili speaker** — stated plainly per the WO's disclosure requirement, not implied as checked. Codex flagged its own highest-uncertainty keys: `MessagingComposer.STAGED_FILES`, `MessagingList.MUTE`/`UNMUTE`, the `MessagingReadTicks.*` receipt wording, `MessagingAttachments.*PREVIEW*`, `MessagingReactions.TOGGLE_*`, `MessagingPoll.YOU_VOTED`, `MessagingConfig.DM_POLICY`/`GROUP_ENABLED`/`EVERYONE_POSTS`. **Independent `reviewer` (Sonnet)**, explicitly scoped to structure/parity/mechanics only (cannot and did not attempt to validate Swahili quality — said so): confirmed all 116 keys have exactly `{de,en,fr,sw}` with `sw` non-empty; **0 mismatches** in a scripted byte-comparison of every `de`/`en`/`fr` value against the pre-change file (additive-only, confirmed); **0 placeholder-token mismatches** — every `{{...}}` interpolation token set in `sw` matches its `en` counterpart exactly, including multi-token keys; **terminology-reuse spot-check confirmed real, not merely claimed** — Save→"Hifadhi", Close→"Funga", and the "could not be X: {{message}}" error-phrasing pattern all matched `notifications`/`onboarding` precedent, no coined alternates found for the named glossary concepts; both test-file diffs minimal and correct. Independently ran the scoped suite: **138/138 messaging tests + 4/4 i18nAggregate tests green.** **`I18N-1`'s recorded follow-up is now closed** — see its own Notiz, updated in this commit. Version bumped 2.41.2 → 2.41.3 (patch, additive strings only, per the WO). **A push to `main` publishes** — do not treat the green tile as the acceptance criterion (see `DX-3`); verify `npm view @micha.bigler/ui-core-micha version` returns `2.41.3` after push. |
64
- | THEME-12 | `ScatterChart` ignores the kit's own margin opinion | `THEME-11` gave the kit an opinion about chart margins instead of inheriting MUI's flat 20 px. **`src/components/charts/ScatterChart.jsx` does not reference `withMarginDefaults`** — verified in the source — even though `THEME-10` shipped it in the same `2.41.0`. So every `ScatterChart` consumer re-inherits exactly what `THEME-11` removed. Found 2026-08-12 while surveying the strand; `hram/FIX-15` already bridged it app-side with a local `CHART_MARGIN`, which moves the workaround into the app instead of fixing the kit — the pattern worth not repeating. | 2026-08-12 | planned | | Tier 3 (shared-core, changes rendered output for every scatter consumer). **Not yet authored** — needs a decision first: wire `ScatterChart` to `withMarginDefaults` and let hram drop `CHART_MARGIN`, or decide scatter legitimately needs different margins and document why. The second is plausible — a scatter with a rotated axis title and auto-sized y-tick width is not a bar chart — so this is a question, not a defect to patch blind. `hram/FIX-15` named the gap in its own notes; this row exists so the kit side is tracked where it belongs rather than only in a consumer's WO. |
65
- | AUTH-5 | `addPublicPath` muss dynamische Routen ausdruecken koennen | `apiClient` leitet bei 401/403 hart auf `/login` um, ausser der Pfad steht auf einer Allowlist — der Mechanismus ist richtig gebaut, kann aber keine **dynamischen** Routen beschreiben: `matchesPublicPath` vergleicht mit `startsWith` (ausser `/`, das Gleichheit verlangt). Eine App mit oeffentlichen Seiten unter einem variablen Segment (`/<slug>`, `/<slug>/team`) kann ihre oeffentliche Flaeche damit nicht ausdruecken: ein konkreter Slug laesst sich eintragen, der naechste faellt wieder heraus. Folge: ein anonymer Besucher landet beim ersten 401 auf `/login` — auf einer Seite, die oeffentlich sein soll, von aussen nicht von "Seite existiert nicht" zu unterscheiden. `addPublicPath` nimmt daher zusaetzlich ein `RegExp` entgegen, geprueft ausschliesslich gegen `pathname`; die eingefrorene Builtin-Liste bleibt string-basiert und unberuehrt. | 2026-08-17 | done | ca09d6e, 2c4a5d3 | Tier 3 (shared-core). reviewer: keine P1/P2-Blocker (ein P2 zu g/y-Flag-Statefulness bei RegExp.test() behoben — lastIndex-Reset + Regressionstest ergaenzt). sec_reviewer: keine Blocker (zwei P3-Hinweise, kein Handlungsbedarf). Kein ui_reviewer noetig (keine Oberflaeche). Veroeffentlicht als 2.41.1 (patch, da Erweiterung einer bestehenden Funktion). jg-ferien PUB-7 kann den Pin jetzt von 2.37.1 auf 2.41.1 ziehen. |
66
- | PRIM-1 | Promote `StatTile` and `SoftChip` into the shared kit | Both already exist **twice**, independently built, and each second build is an **inline component inside a page** rather than a file — `StatTile` in `fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:554-568` beside hram's `ResearchComponents/StatTile.jsx`, `SoftChip` in `cockpit/frontend/src/cockpit/BoardView.jsx:65-73` (the lane pill) beside hram's `SoftChip.jsx`. The promotion trigger is met on evidence, not waived. The two builds diverged in seven places; all seven decided by the operator 2026-08-12 and rendered in the prototype. Full order [`work-orders/PRIM-1.md`](work-orders/PRIM-1.md) | 2026-08-12 | done | b5fff23 | Tier 3 (shared-core). `reviewer` + `ui_reviewer` mandatory and concurrent; no `sec_reviewer`. **Prototype in scope** — [`work-orders/assets/PRIM-1-dashboard-primitives.html`](work-orders/assets/PRIM-1-dashboard-primitives.html), so the rendered 375/1280 side-by-side is a commit gate; screenshots have not been legibly obtainable here, so a substitution must be **declared**, not silent. Decisions: value `h5` 18/600 · `tabular-nums` always · `flex: 1` with a `minWidth` floor · `accent` kept (fitness-monitor cannot adopt without it) · responsive padding · `status` uses **`overline`** (cockpit independently chose uppercase + .4px tracking, so this satisfies `THEME-4` with **no** baseline change and only hram's chips shift to uppercase) · dot-or-icon and optional border via props. **Token delta list has one entry:** `caveat`'s 12/500 has no baseline variant (`caption` is 12/400) — operator resolved it as keep-the-value, so it must be **exempted in `THEME-4` with that reason stated**, not flattened to 400 and not fixed by extending the baseline here. Neither `StatTile` build has a test; the promoted tests are written from scratch. Minor bump `2.38.0` → `2.39.0` (new capability area). **The three app adoptions are separate follow-up WOs** and land no app change here. Programme strand `DS-5`. **Codex skipped, dated evidence**: `.claude/codex-status.md` records `unavailable` for 2026-08-12 (`cockpit/UI-10`, out of workspace credits, the day's one allowed probe already spent) — Orchestrator implemented directly, flipping authorship. **Independent `reviewer` (Haiku, mandatory regardless of tier per the WO): no findings.** **`ui_reviewer` (Haiku, mandatory, concurrent): two P1 findings, both fixed** — (1) `StatTile`'s padding had only `px` responsive (`{xs:1, sm:1.5}`) with `py` fixed at 1, against the decided "responsive `{xs:1, sm:1.5}`" (matching fitness-monitor's original `p` shorthand, not hram's split values) — changed to a single `p: {xs:1, sm:1.5}`; (2) the `status` variant's typography was under-tested (only `textTransform` checked) — added explicit `fontSize`/`fontWeight`/`letterSpacing` assertions against `theme.typography.overline`, and separately against the `caveat` variant's raw 12/500, both now green. Re-verified 51/51 scoped tests green after fixes, `tsc -p tsconfig.build.json --noEmit` clean. **Rendered gate: declared substitution**, per the WO's own text — the 375/1280 side-by-side screenshot capture is not legibly obtainable in this repo; computed-style/DOM inspection substituted instead, via the test suite itself (fill/border/text colour per tone verified against `alpha`/`darken` of `theme.palette[tone]`; `borderRadius` 999px status vs non-pill caveat; `fontVariantNumeric: tabular-nums`; accent border width 1px vs 2px; `overline` typography values now explicit). No visual click-through was performed. Commit `b5fff23`. |
67
- | THEME-3 | Add the missing "subtle surface" baseline token | Adds `palette.background.subtle` (`#F4F5F6`, fixed baseline constant alongside `default`/`paper`) to `createAppTheme` and registers it in `THEME_COMPLETENESS_SURFACES`. Found by `cockpit/UI-6`: the `UI-4` design-language sheet's canonical token table names this as a baseline-supplied "inset surface" replacing `cockpit.surface2` (16 of 195 token reads in that WO), but the value was never actually wired into the shipped package — confirmed by grep, not assumed. `cockpit/UI-6` is blocked on this landing + publishing first, per this estate's own "gap in the shared kit is a ucm WO, not a local workaround" rule. Full spec: [work-orders/THEME-3.md](work-orders/THEME-3.md). `2.33.2` (patch). | 2026-08-11 | done | 1b25117 | Tier 3 — shared-core, this repo publishes to npm on every `main` push touching `src/**`, no staging net. Codex implemented in one pass (stdin invocation), exit 0, `RESULT: DONE`, 17/17 of its own new tests green. **Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently — both clean, no findings.** Both independently re-derived the safety mechanism rather than trusting the WO's claim: `MUI_DEFAULT_THEME = createTheme()` never defines `background.subtle` (vanilla MUI only has `.default`/`.paper`), so `assertThemeComplete`'s `sameValue` check can never flag the new surface for any real adopter — the same unexempted treatment `background.default` already gets; `background.paper`'s exemption (needed because MUI's own default theme is also white) was correctly not copied. Both read `fitness-monitor`'s actual `theme.js`/`theme.test.js` directly (the one real production adopter, hard-asserting `assertThemeComplete(theme).findings` equals `[]`) and confirmed this diff cannot break it. `ui_reviewer` additionally grepped every sibling app repo under `webapps/` for `createAppTheme` — no other live consumer exists; cockpit's own adoption (`UI-6`) isn't wired yet. `reviewer` independently recomputed the contrast ratio in a throwaway script and got the same `5.3748:1` the diff claims (not the design sheet's `4.81:1`, which was computed on the sheet's own static canvas — a source discrepancy, not a functional risk; both clear AA). **Orchestrator's own verification:** built the package (`npm run build`) and attempted to run `fitness-monitor`'s real assertions against the fresh `dist/` via raw Node ESM import — hit a pre-existing (confirmed via `git show`, not introduced here) `tsc`-emitted extensionless-import quirk that only affects raw `node`, not the Vite/Vitest resolution every real consumer (incl. fitness-monitor's own suite) uses; fell back to the scoped vitest run instead. Tests: 17/17 green (`tests/createAppTheme.test.js` + `tests/themeCompleteness.test.js`), run by the Orchestrator. No visual/prototype gate — token-only change with no rendered specimen in this repo (per the Envelope); the rendered check happens downstream in `cockpit/UI-6`. Semver: patch, applied by the Orchestrator per the WO's own reasoning (additive, resolves automatically, no adopter code change needed) — confirmed rather than assumed by both reviews. Unblocks `cockpit/UI-6`, which was waiting on this landing + publish. |
68
- | CHART-7 | `ChartFrame` gains a `titleVariant` prop | Adds `titleVariant` (default `'h6'`, unchanged from today's hardcoded value) to `ChartFrame`, forwarded to the title `Typography` — lets a dense panel grid (e.g. fitness-monitor's seven body-composition panels beside one hero chart) render smaller titles that recede instead of competing with the one chart that matters. One prop, not a `slotProps.title` system — deliberately minimal per the WO. Precondition of fitness-monitor's `FM-12` (DS-10). `2.32.0` (minor, additive). Full spec: [work-orders/CHART-7.md](work-orders/CHART-7.md). | 2026-08-10 | done | 14020d0 | Envelope authored by Expertenchat, no design question (short-form WO). No preconditions. Orchestrator filled a minimal Implementation map (the whole change is two lines in `ChartFrame.jsx`), ran Codex (stdin invocation). Codex added `titleVariant = 'h6'` to the prop destructure, forwarded it to the title `Typography`, wrote all 3 required tests, and bumped `2.31.1` → `2.32.0` per the WO's own stated version target. **Independent `reviewer` + `ui_reviewer` (both Haiku, proportionate to a small low-risk diff) ran concurrently — both found NO findings**, including explicit confirmation that only the prop-destructure line and the one `Typography` line changed (padding, foot row, subtitle/toolbar/slots byte-for-byte unchanged) and that the default-regression test is non-vacuous. Re-verified: 13/13 scoped vitest tests green, `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)**: no existing harness specimen used `titleVariant`, so the Orchestrator added one (two `ChartFrame`s side by side, default vs. `titleVariant="subtitle2"`) to `dev/entries.jsx` — confirmed live at 375px and 1280px that the override renders visibly smaller/lighter than the unchanged h6 default. |
69
- | THEME-2 | Two defects from the THEME-1/CHART-6 landings: `warning.main` contrast, `grid` replacing instead of merging | Fixes `palette.warning.main` (`#C08A2C`, 3.038:1 on white — fails AA) to `#976100` (the existing `text` tone, ~5.2:1), `contrastText` to `#FFFFFF`; `warning.fill`/`.text`/`.fillText`/`.bg` unchanged. Adds a completeness-assertion rule checking every status channel's `.main` against `#FFFFFF` and `background.default` at 4.5:1. Fixes `BarChart`/`LineChart`'s `grid` prop silently replacing (not merging) a caller's partial grid object; the required audit of sibling object-valued defaults found a second live instance (`legendPosition`, same bug class) and fixed it too. `2.31.1` (patch). Full spec: [work-orders/THEME-2.md](work-orders/THEME-2.md). | 2026-08-10 | done | 7d5039f | Envelope authored by Expertenchat, both defects fully diagnosed up front (no design question). Preconditions (THEME-1 `2.30.0`, CHART-6 `2.31.0`) verified. Orchestrator filled the Implementation map with the exact current-state code pointers for both defects, then ran Codex (stdin invocation). Codex's own audit (per the WO's "close the mechanism" instruction) found the `legendPosition` partial-override bug beyond the named `grid` fix and fixed it too, exactly as the WO anticipated ("fix that too and say so"). **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently — both found NO findings**, including the reviewer's own hand-computed contrast check confirming `#976100` clears 4.5:1 against both `#FFFFFF` (~5.22:1) and `background.default` (~5.00:1). One thing the Orchestrator itself fixed (not a reviewer finding): a pre-existing THEME-1 regression test (`tests/createAppTheme.test.js`) asserted `palette[status].main === palette[status].fill` for all four status channels — THEME-2 deliberately breaks that equality for `warning` only (the whole point of the fix), so the assertion was updated to special-case `warning` against its new intended invariant (`main === text`, `contrastText === '#FFFFFF'`) while `success`/`error`/`info` keep the original assertion. Re-verified after the fix: 58/58 scoped + adjacent vitest tests green (`tests/themeCompleteness.test.js`, `tests/chartDefaults.test.js`, `tests/createAppTheme.test.js`, `tests/chartsPalette.test.js`, `tests/BarChart.test.jsx`, `tests/LineChart.test.jsx`, `tests/TimeSeriesChart.test.jsx`), `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's "Theme / Shared baseline" entry — the WO's own point was that no existing specimen exercised `.main` (the `StatusChip`'s `sx={{bgcolor:'...fill'}}` and the `MuiAlert` `standardWarning` override both bypass it), so a new Button/Chip/`Alert variant="filled"` specimen row was added and confirmed live at 375px and 1280px: the warning tone now renders visibly darker (brown, white text) than the still-unchanged lighter-amber `fill`-based chip beside it. The "Caller overrides" `LineChart` specimen's `grid` prop was also changed from a complete `{horizontal:true,vertical:true}` object (which wouldn't have visually proven the merge) to a genuinely partial `{vertical:true}` one — confirmed live at both widths showing BOTH grid directions post-fix. |
70
- | CHART-6 | Chart-chrome defaults in the three wrappers, plus two generic fixes hoisted up | Closes the chart-chrome default surface in `BarChart`/`LineChart`/`TimeSeriesChart`: baseline tick font size (from `theme.typography.caption`), a horizontal grid, `showMark:false` with a filled-marker fix when a caller opts markers back in, automatic bottom-margin growth for rotated tick labels, a new `legendPosition` prop (default bottom-start), and `TimeSeriesChart`'s own toolbar becoming the sole legend (MUI's redundant second legend removed). Hoists `chartLabels.js` (short granularity-aware tick-label formatters) and a generalised `yearTickInterval` from fitness-monitor. `2.31.0`. Full spec: [work-orders/CHART-6.md](work-orders/CHART-6.md). | 2026-08-10 | done | 6dedc62 | Envelope authored by Expertenchat (DS-4). Precondition (THEME-1 landed + published `2.30.0`) verified before start. Orchestrator filled the Implementation map with concrete MUI X-Charts source pointers (installed `8.29.2`, not the WO's stated `8.28.2` — flagged and verified against the actual installed source throughout), then ran Codex (stdin invocation, same as THEME-1). Codex implemented `src/components/charts/{chartDefaults,chartLabels,yearTickInterval}.js`, wired all three wrappers, wrote the 10 required tests, and used a wrapper-level `slots.mark` composition for the filled-marker fix (a deliberate, now-documented deviation from the WO's named `MuiMarkElement` theme-token mechanism — the wrapper-level fix applies to all 14 consumers immediately on this version bump, not only to apps that separately adopt `createAppTheme`). **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently.** `reviewer` found **R1 (P2)** — the marker-fill deviation from the WO's specified mechanism was undocumented; fixed with an explanatory code comment (functionally correct either way, this was a disclosure gap, not a bug). `ui_reviewer` found **U1 (P3)** — `SeriesToggleDot`'s 12px dot used hardcoded pixels instead of `theme.spacing()`; fixed. Both reviewers otherwise found the six highest-risk invariants (default-vs-caller-override merge order, filled markers, rotated-tick margin math, the double-legend fix, `yearTickInterval`'s generalisation, `skipAnimation` passthrough) correct and non-vacuously tested. **A third, more serious bug was found by the Orchestrator's own DS-1 two-width rendered check (not by either reviewer, since both are static-diff reviews)**: `withAxisDefaults` in `chartDefaults.js` only applied `defaults.scaleType` when the caller passed NO `xAxis`/`yAxis` array at all — a caller-supplied axis with `data` but no explicit `scaleType` (exactly what the new dev-harness "Chart defaults" `LineChart` specimen used) silently lost the wrapper's `'point'`/`'band'` scale default entirely, leaving MUI's scale undefined and every rendered line/point position `NaN` (`<path> attribute d: Expected number, "MNaN,...`, confirmed live via `read_console_messages` and only reproducible through a real MUI scale computation — a mocked prop-assertion test structurally cannot catch it, the same class of bug CHART-1/3/4/5 have each found before in this kit). Fixed by spreading `defaults` before `axis` in the per-axis merge (caller's own explicit `scaleType`, if any, still wins); a new regression test added (`tests/chartDefaults.test.js`, "keeps the default scaleType on a caller-supplied axis that does not set one"), confirmed against both `BarChart`'s `'band'` and `LineChart`'s `'point'` defaults. Re-verified after all three fixes: 44/44 scoped vitest tests green (10 new + 34 existing adjacent chart tests, no regressions), `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's "Theme / Shared baseline" entry (which now also carries the two new `LineChart` chart-chrome specimens per the WO's "at least one chart that set the props and one that did not" requirement) at 375px and 1280px — confirmed live: correct line rendering (post-fix), filled markers on the caller-override chart, horizontal grid, bottom-start vs. caller-chosen legend position, and rotated tick labels with adequate margin at 1280px. |
71
- | THEME-1 | Shared MUI theme baseline: `createAppTheme` factory + completeness assertion | Ships `createAppTheme(appConfig)` (palette-first merge of a fixed light-mode baseline with the app's own `palette`/`typography`/`components`; throws without `palette.primary`; throws on a function anywhere in `appConfig.components.*.styleOverrides`), an exported `assertThemeComplete` completeness assertion with an exemption arm and contrast checks (status text-on-own-tint, `controlBorder` on white + page background), a non-breaking chart-ramp fallback in `useNeutralChartPalette` (adopters get the theme's `dataSeries` ramp, non-adopters see byte-identical output), a new "Shared theme baseline" dev-harness entry, and `DESIGN.md` principles 1/8 reworded per the accepted policy change (ucm now ships a shared theme; the data-colour layer has a shared default). Full spec: [work-orders/THEME-1.md](work-orders/THEME-1.md). | 2026-08-10 | done | b329b39, 96992cf | Envelope authored by Expertenchat from the accepted reference sheet (`work-orders/assets/THEME-1-baseline-reference-sheet.html`, DS-2 decisions 1–23). Orchestrator filled the Implementation map, then ran Codex (stdin, not positional arg — the WO text exceeded Windows' command-line length limit via the npm shim, "Argument list too long"; `codex exec ... -` reading the WO from stdin is the working invocation for large WOs on this platform). Codex implemented `src/theme/{tokens,createAppTheme,themeCompleteness,index}.js`, wrote all 13 required tests + extended `tests/chartsPalette.test.js`, updated `src/index.js` exports, `DESIGN.md`, `package.json` (2.29.2→2.30.0, new `@fontsource/dm-sans` dependency — WO-scoped, no separate approval needed), and the dev harness. **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently. `ui_reviewer`: no findings.** `reviewer` found three, all confirmed real and fixed by the Orchestrator: **R1 (P2)** — MUI's `createTheme(options, ...args)` only runs palette augmentation on the FIRST argument; every later arg (how the baseline was layered in) is deep-merged raw, so `success`/`warning`/`error`/`info` — which had `text`/`fill`/`fillText`/`bg` but no `main`/`contrastText` — silently kept MUI's own stock hues for any standard `color="success"` usage. Verified live via a Node repro (`createTheme({...},{palette:BASELINE_PALETTE},{...})` on the pre-fix baseline produced `success.main:'#2e7d32'`, MUI's stock green, not the canonical `#1B8038`). Fixed: explicit `main`/`light`/`dark`/`contrastText` added to all four status channels (`light`/`dark` computed via MUI's own `lighten`/`darken` at the same 0.2/0.3 tonal offsets `augmentColor` uses), plus a new regression assertion in `tests/createAppTheme.test.js`. **R2 (P3)** — `controlBorder.focus` was derived to clear 3:1 only against `background.paper`, while `assertThemeComplete`'s own contrast check tests it against `#FFFFFF` and `background.default` — an app overriding either background could pass the derivation and still fail the assertion. Fixed: derivation now targets both surfaces. **R3 (P3)** — `MuiOutlinedInput`'s `variants` array was nested inside `styleOverrides.root` instead of as a sibling key; MUI never reads `variants` from inside `styleOverrides` (it's matched by `props` independently), so the entry was inert dead code, and it also meant the function-in-`styleOverrides` guard's own suggested escape hatch ("use MUI variants") was structurally unreachable from inside `styleOverrides`. Fixed: moved to the correct sibling placement; updated the one test and the one completeness-surface path that referenced the old (wrong) location. Re-verified after fixes: 15/15 scoped vitest tests green (`tests/createAppTheme.test.js`, `tests/themeCompleteness.test.js`, `tests/chartsPalette.test.js`), `tsc -p tsconfig.build.json --noEmit` clean. Orchestrator's own security/scope pass: no findings (only the WO-approved `@fontsource/dm-sans` dependency addition, no secrets, no scope creep). **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's new "Theme / Shared baseline" entry at 375px and 1280px — both screenshots confirm radius (3/8), no resting shadow on Paper/Card, correct status fill/text/bg tones, and the six-colour series ramp, consistent with the reference sheet. No app pin bumps in this WO — all 14 consumers pin exact versions and pick this up on their own adoption WO. |
72
- | CHART-5 | Dual y-axis + always-visible x-axis ticks on `TimeSeriesChart` | Opt-in second y-axis via a new per-series `axis: 'secondary'` field (backward-compatible, off by default) with auto-detected integer-only tick labels per axis group; deterministic x-axis `tickLabelInterval` so bucket labels never all vanish under MUI's default collision-avoidance (the live symptom: zero x-axis labels with 24 hourly buckets). Reverses CHART-2's deliberate "no second y-axis" choice on fresh operator instruction; `DESIGN.md` #8 updated to record the scoped exception. Full spec: [work-orders/CHART-5.md](work-orders/CHART-5.md). | 2026-08-07 | done | 839e6c5, 84c37ac, 0dd3bac, f4e5586, 4a12a71 | Operator-requested after seeing the shipped chart live: y-axis showed decimal ticks (0.2, 0.4, ...) for an integer user-count series sharing an axis with fractional hours; x-axis showed no time labels at all. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). Independent `reviewer` + `ui_reviewer` ran concurrently: **`reviewer` found R1 (P1, real bug)** — used `yAxisKey` (removed in MUI X-Charts v7, per the installed v8.29.2's own CHANGELOG and `useBarPlotData.mjs` source, which reads `series[i].yAxisId`) instead of `yAxisId`; the WO explicitly asked to verify this passthrough against real MUI behaviour rather than assume, and the first pass didn't — the dual-axis feature would have silently plotted every series on the primary axis in production despite all mocked tests passing (a mocked-prop test structurally cannot catch a wrong-field-name bug, since it only checks whatever the code itself sets). Fixed (`yAxisKey` → `yAxisId`, verified against `node_modules` source directly, not just the mock) and the pre-existing `tests/BarChart.test.jsx` "dual axis" case corrected too (R2 — it used the same wrong field name and was cited by the WO as precedent, giving false confidence). **`ui_reviewer` found U1 (P1, real bug)** — `tickLabelInterval`'s `index % step === 0` systematically dropped the LAST x-axis bucket unless it landed exactly on a step boundary (e.g. 24 labels/step 3 → indices 0..21, never 23), exactly the failure mode the WO's own SCOPE C explicitly warned to check for; fixed by force-including `index === lastIndex`, with a new `it.each([24,12,7,1])` regression test, confirmed non-vacuous by reverting the fix. **U2 (P2)**: `DESIGN.md` #8's "Never dual-axis" now contradicted the shipped code — updated to record dual-axis as a scoped, opt-in exception (CHART-5) rather than a general rule change. U3 (P3, fixed `MAX_X_TICKS` under-displays low-density ranges e.g. 12 monthly buckets showing only 6) and U4 (P3, no UI indication of which axis a toggle's series plots against) both accepted, not fixed — informational, no code change. Re-verified after fixes: 31/31 scoped vitest tests (`TimeSeriesChart.test.jsx` + `BarChart.test.jsx`) green, `tsc` build clean. Published: ucm 2.29.0 (minor — new optional per-series field + prop, additive, no breaking change). **Third bug found post-publish (2.29.1 patch)**: after jg-ferien adopted it (ACT-7) the operator reported live that the secondary axis was "obviously there but invisible" and x-axis labels were still missing. Investigated by adding a new DX-1 harness entry (`dev/entries.jsx` `TimeSeriesChartEntry`, previously missing — this component had NEVER been visually verified in a real browser through CHART-2/3/4/5) and reading the real rendered DOM directly. X-axis confirmed working (9 evenly-spread labels for 24 buckets) — the live report was very likely a stale PWA service-worker cache on jg-ferien's side, not a code defect. Y-axis confirmed broken: only 1 of 2 `yAxis` entries rendered in the DOM. Root cause traced to the installed MUI X-Charts source (`defaultizeAxis.js`): `defaultPosition = index === 0 ? 'left' : 'none'` — every yAxis array entry past the first silently renders nothing unless given an explicit `position`, a class of bug no mocked-`BarChart` test can catch (the mock never runs MUI's own defaulting logic). Fixed by setting `position: 'left'`/`'right'` explicitly on the primary/secondary entries; independent `reviewer` traced the fix against the same MUI source and confirmed no second gating layer exists. Live-reverified in the dev harness after the fix: both axes render with independent scales (primary `0,1,2`; secondary `0.0,0.5,1.0,1.5,2.0`). 32/32 tests green, build clean. Published: ucm 2.29.1 (patch). **Fourth report, root cause NOT in ucm**: operator reported x-axis labels still empty even after 2.29.1. Extensive live investigation (scoped DOM queries, computed-style checks, a real-browser-DOM comparison against this repo's own dev harness in both dev and production-build mode, a DM-Sans-not-loaded font test) could not reproduce it anywhere in ucm with the exact real bucket data — ruled out: stale cache, MUI axis config, container width, dev-vs-prod build, font-loading timing. Root cause found via web search: a known, unfixed upstream MUI X-Charts bug (mui/mui-x#18768, duplicate of #18399) where insufficient measured space makes its `ellipsize` logic collapse an ENTIRE tick label to `''` instead of shortening it, confirmed live via a `<tspan>` with correct styling but zero text content — triggered by jg-ferien's verbose label format ("08/05, 10:00 AM", 16 chars) combined with its actual DM Sans font metrics (not reproducible with this repo's default Roboto-based harness). Fixed in jg-ferien (ACT-9: short, granularity-aware labels), not in ucm — `TimeSeriesChart` itself has no defect here, it's a host-side label-length choice interacting with an upstream MUI bug. This repo's DX-1 harness entry (`TimeSeriesChartEntry`) updated to model the correct short-label pattern so it doesn't teach the risky verbose format. |
73
- | CHART-4 | Fix `TimeSeriesChart` chart canvas rendering at zero height | Neither `ChartFrame` nor `BarChart` ever receive a height from `TimeSeriesChart` — MUI X-Charts' responsive container measures a zero-height parent and draws no bars/axes, while the legend (sized by its own content) renders regardless, which is why the live symptom was "legend shows, chart canvas doesn't". Fix supplies a preset default height (`CHART_HEIGHT = 320`, `minHeight` on `ChartFrame` + `height` forwarded to the real `MuiBarChart`), matching the working `dev/entries.jsx` reference pattern. Full spec: [work-orders/CHART-4.md](work-orders/CHART-4.md). | 2026-08-07 | done | f0953a1, fcb289b | Second, independent bug found live in jg-ferien immediately after CHART-3 + the ucm pin bump landed — CHART-3's own fix is what let this one become visible for the first time (it was previously masked by the permanently-empty-chart bug). Root cause of it shipping twice undetected: every test in `tests/TimeSeriesChart.test.jsx`/`tests/BarChart.test.jsx` mocks the real `@mui/x-charts/BarChart`, so no test has ever mounted the real chart and observed its size. **Codex out of credits, known up front** — implemented directly (author-flip, independent review mandatory). **Test-writing pitfall found and self-corrected during implementation**: first wrote an *unmocked* render test asserting the real chart SVG's `viewBox` height was non-zero — verified vacuous (stays green even fully reverting the fix, since jsdom's `ResizeObserver`/layout handling doesn't reproduce the real zero-height-parent browser behavior this bug depends on). Deleted; replaced with a `chartSpy`-mocked prop assertion (`props.height` non-zero) matching the file's existing convention, confirmed non-vacuous (fails pre-fix with `props.height` undefined). Independent `reviewer` + `ui_reviewer` ran concurrently: `reviewer` verified the fix mechanism end-to-end (no prop-name collision between `BarChart`'s own `minHeight`/`aspect` and the forwarded `height`) and the test's non-vacuity claim, no findings. `ui_reviewer`: two P3 informational notes, neither blocking — (U1) the fixed 320px height is a deliberate, documented exception to `DESIGN.md` #14 ("responsive = container-sized, not fixed px"), needed specifically because MUI X-Charts' own `ResizeObserver`-based height measurement is what caused CHART-4 in the first place; a code comment was added warning future readers not to "fix" this back to a responsive height without re-verifying the underlying MUI bug is resolved. (U2) `CHART_HEIGHT` uses 320 for both `ChartFrame.minHeight` and `BarChart.height`, vs. `dev/entries.jsx`'s 360/320 split — functionally harmless (WO explicitly permits one fixed value), no change made. 13/13 scoped vitest tests green, `tsc` build clean. Published: ucm 2.28.2 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
74
- | CHART-3 | Fix `TimeSeriesChart` rendering nothing on async data load | `visibleKeys` was a lazy `useState` initialized once from `data` at first mount; any host that fetches asynchronously after mount (the normal case, e.g. jg-ferien's `ActivitySection`) ends up with a permanently-empty `visibleKeys`, so `isDataEmpty` stays `true` forever and the chart body never renders even once real data arrives. Fix syncs newly-seen series keys to visible-by-default while preserving a user's manual toggle-off across data updates. Full spec: [work-orders/CHART-3.md](work-orders/CHART-3.md). | 2026-08-06 | done | 9a1bd9d, 48cefa8 | Found live in jg-ferien's Aktivität screen (ACT-3 consumer) — toolbar rendered, chart body stayed on `ChartFrame`'s empty state permanently. **Codex was out of workspace credits** (genuine quota error on invocation, no partial work) — orchestrator implemented directly per the Codex→Claude fallback rule; independence flip applies (author = orchestrator, so independent review was mandatory, not self-check). Independent `reviewer` + `ui_reviewer` both ran (background, concurrent): `ui_reviewer` found no findings; `reviewer` found R1 (P2, real bug — the seen-keys tracker was a `useRef` mutated during the render body alongside a paired `setVisibleKeys` call; under React concurrent rendering an interrupted/discarded render leaves the ref mutation applied while the paired state update is discarded, permanently hiding a key without it ever reaching `visibleKeys` — a different-shaped recurrence of the exact bug this WO exists to fix) and R2 (P3, test-file duplication — a second near-duplicate render helper). Both fixed: seen-keys tracking moved from a ref to a second `useState` (both states updated together in the render body, so a discarded render rolls both back together — React's documented pattern for this), and the duplicate test helper collapsed into the existing `renderChart`'s `rerenderWith`. Re-verified: 12/12 scoped vitest tests green (3 new CHART-3 regression tests, 2 of which confirmed non-vacuous by failing against the pre-fix code), `tsc` build clean. Published: ucm 2.28.1 (patch, no interface change). No app pin bumps in this WO — jg-ferien picks it up on its next ucm bump. |
75
- | ONB-1 | Per-app configurable notifications onboarding step | New `browserPush` prop `{nagUntil, showOnce}` on `OnboardingProvider`; parameterizes the `browser_push` descriptor's condition (default changes from implicit "all-channels" to "any-channel", stopping the over-nag); `showOnce` via a persisted `onboarding_seen` set (frozen-at-mount ref, no mid-session flicker) | 2026-07-17 | done | 1ae7c20 | Default behavior change affects cockpit and all consumers on their next ucm bump. jg-ferien companion WO pins the new version with `browserPush={{nagUntil: 'any-channel'}}` explicit (matches new default, but pinned explicitly per the WO). |
76
- | PERF-3B1 | Parallel auth bootstrap after CSRF | Starts auth-methods and current-user concurrently once CSRF is available, while preserving error handling and loading semantics. | 2026-07-19 | done | ee89329 | Independent `reviewer` + `sec_reviewer` passes both clean (no findings); one P3 test-coverage gap from the reviewer (missing mirror case: auth-methods rejects, current-user succeeds) closed with an added regression test — 45/45 tests green. Published as 2.10.1 (patch, no interface change) and pinned in jg-ferien alongside its PERF-3A companion WO. |
77
- | CHART-1 | Shared chart kit: themed `ChartFrame` + MUI X-Charts Bar/Line presets | Typed-agnostic `ChartFrame` (Paper + title + toolbar slot + loading/error/empty + responsive container + SVG/PNG export + a11y) wrapping any chart body via `children`; thin `BarChart`/`LineChart` presets over MUI X-Charts baking the governance chart contract (both-axis labels, legend-when-multiseries, tooltip, theme-token colours, responsive); neutral theme-aware palette + locale-aware formatters. Additive exports; publishes ucm 2.15.0. | 2026-07-30 | done | f1b3614 | Envelope authored by Expertenchat; `work-orders/CHART-1.md`. Operator approved the `@mui/x-charts` peerDependency 2026-07-30 (in-chat) before Codex ran. Codex-built direct (no nesting). Independent `reviewer` (background, author=Codex ≠ reviewer): found R1 (P1, real bug — 3 of 4 WO-required tests failed on actual execution: no RTL `cleanup()` between `render()` calls left stale DOM across the `ChartFrame` state-precedence/empty tests, and the `BarChart` responsive-sizing assertion called `.not.toContain(null)` on a `getAttribute('style')` that MUI's `sx` never sets inline — an invalid matcher call), R2 (P2 — `BarChart`/`LineChart` passed an invalid `tooltip={{trigger:'axis'}}` prop that isn't part of either component's real API per installed `@mui/x-charts@8.29.2` types; silently worked only via the library's own default, and the mocked tests couldn't catch it), R3 (P2 — `ChartFrame`'s chart region set both `aria-label` and `aria-labelledby`; per ARIA accessible-name computation `aria-labelledby` wins, so a caller-supplied `ariaLabel` override was silently discarded by assistive tech), R4 (P3 — the PNG export control was never actually clicked in the export test, only checked for presence). All four fixed by the orchestrator directly (in-scope reviewer-finding fixes, not a re-implementation): added `afterEach(cleanup)` to the three render-based test files, fixed the responsive-sizing assertion to tolerate `null`/empty inline style, moved tooltip config to the real `slotProps={{tooltip:{trigger:'axis'}}}` API (tests updated to match), suppressed `aria-labelledby` whenever a custom `ariaLabel` is supplied (with a new regression test), and added a PNG-export regression test asserting the callback fires. Re-verified: 16/16 scoped vitest tests green, `pnpm`/tsc build clean. hram adoption (pin bump + ~20 Results/Research panel migration) is a gated follow-on WO in the hram repo, gated on 2.15.0 published (publish-from-main CI on this push). SVG specialists (scatter/tornado/heatmap/CI/frontier) get frame+export in adoption but keep raw-SVG bodies — retiring raw SVG is a tracked future CHART-2. |
78
- | NOTIF-6 | NotificationsProvider + bell on canonical dcm feed/* API | Single-WS-owner `NotificationsProvider`/`useNotifications` (initial REST seed via `feedApi.js` + one `/ws/notifications/` socket, reconnect w/ backoff) + generic MUI `NotificationBell`; additive exports only | 2026-07-27 | done | 6c63fb9 | Codex-built direct (no nesting), independent `reviewer`: single-WS-owner invariant traced clean (no way to break it — `authenticated`/`replaceNotifications`/`refresh` all referentially stable across unrelated re-renders, re-render test is genuine not a no-op). R1 (P2, real bug): the `notification.status` WS handler patched item flags but never adjusted `unreadCount`, so the badge went stale after a cross-tab/cross-device mark — the test had locked in the stale count as expected behavior. Fixed: delta computed from before/after unread state on the WS patch path, mirroring `mark()`'s own logic; test corrected + a new cross-tab regression test added. 57 passing, build clean. No new npm dependency. Full history + backend contract: dcm's `WORK_ORDERS.md`/`docs/design/notifications-platform.md`. Publishes ucm 2.11.0 |
79
- | NOTIF-13 | Layer-1 transport extraction (pluggable realtime) | Extract the socket out of `NotificationsProvider` into a generic **subscribe-by-envelope** realtime primitive (shipped API: `useRealtime() -> subscribe(envelope, handler)`; the WO said "by message-type", but routing is on the `envelope` discriminator, deliberately NOT on the overloaded `type` field — that overload was the bug) so a second domain (messaging) can ride the same socket; the catch-all that turns every non-`notification.status` message into a feed entry is replaced by explicit dispatch with unknown types ignored. Additive: `NotificationsProvider` stays the sole mount point, its context value unchanged. Full spec: [work-orders/NOTIF-13.md](work-orders/NOTIF-13.md). | 2026-07-29 | done | ucm: 7a83ee9; dcm: de77335 | Tier 2 (cockpit consumes `NotificationsProvider` at 2.12.0 in prod). Phase A, the hinge: unblocks NOTIF-15 (jg bell adoption) and is the substrate for Phase B messaging. Canonical register row: django-core-micha `WORK_ORDERS.md`. Operator decision 2026-07-29: additive only, NO standalone `RealtimeProvider` apps must mount. Scope E (envelope contract + helper + doc) also touches dcm; note that the roadmap wording "dcm multi-stream consumer" overstates it — `push_to_users`/`NotificationConsumer` are already stream-agnostic. Codex failed twice on this WO with a genuine CLI/model-version error (`gpt-5.6-terra requires a newer version of Codex`), not a transient issue — orchestrator fell back to direct Claude implementation per the Codex→Claude fallback rule. Independent `reviewer` (sec_reviewer not required per WO): found R1 (P2, real bug — decoupling the REST seed from the socket connect opened a race where a live push arriving before the initial feed loaded would be silently discarded by `refresh()`'s unconditional `replaceNotifications()`; fixed via a `seeded` state gate restoring the exact original seed-then-connect ordering, re-verified 70/70 green), R2 (P3 — the WO's "must be called out in the CHANGELOG" has no home since ucm has no CHANGELOG.md convention at all; operator decision: document via commit message + this register entry only, no new file), R3 (register housekeeping, closed by this update). ucm full suite 70/70 + `pnpm build` clean; dcm notifications suite 92/92, full dcm suite 392/392. Published: ucm 2.13.0 (npm), dcm 2.33.0 (PyPI, verified live before this record) — no app pin bumps in this WO per spec. |
80
- | NOTIF-12 | Popup channel via the wizard renderer | Make `popup` a real delivery channel: extract the presentational dialog shell out of `OnboardingWizard`, render popup-eligible notifications through it, and record seen/dismissed on `NotificationRecipient` (design D-F7: renderer reuse only, NOT the onboarding progress store); dcm `PopupDispatcher` stops being a logging stub. Full spec: [work-orders/NOTIF-12.md](work-orders/NOTIF-12.md). | 2026-07-29 | done | ucm: c8e222f, d7bdcbe; dcm: django-core-micha 1612429, 82fd89f | Tier 2 (extracts a renderer cockpit+jg run in production; the roadmap label "uncritical" is about urgency, not blast radius). **HARD GATE: starts only after NOTIF-13 landed + published** — consumes its envelope contract and `subscribe()`, and edits the same ucm files (AGENTS.md overlap rule). **SHIPS WITH ZERO PRODUCERS, deliberately** (operator decision 2026-07-29, made with the evidence): no notification type declares `popup` in `eligible_channels` anywhere in dcm/jg/cockpit/hram/spesix/survey_app, so the channel is wired but inactive until an app opts a type in. WO therefore requires (a) CHANGELOG wording that does not overclaim (NOTIF-4 R1 precedent) and (b) a test-local producer type proving the path end-to-end, so it is not untested dead code. Was NOTIF-11 (renumbered 2026-07-29 for the jg cutover/drop split). **Gate satisfied 2026-07-30** (ucm 2.13.0 / dcm 2.33.0); WO re-verified against the landed NOTIF-13 code and corrected in `345631d`: NOTIF-13 shipped the envelope as a **domain**-level discriminator, so a popup payload built the same way as a chip payload would be indistinguishable from it - the WO now mandates a `channel` field inside the envelope (never a second envelope value) plus feed de-duplication by `notification_id`, without which chip+popup double-counts the unread badge. **Codex failed twice with the same genuine CLI/model-version error as NOTIF-13** (`gpt-5.6-terra requires a newer version of Codex`) — orchestrator implemented directly in Claude per the Codex→Claude fallback rule, both scopes. Independent `reviewer` (background agent, `fork_context=false`, full diff of both repos): found R1 (P1, real bug — the WS payload only carried `notification_id`, but `feed/mark/` resolves ids against `NotificationRecipient.pk`; every `markSeen`/`markDismissed` from a session-live-only popup would either no-op or corrupt an unrelated recipient row once a producer opts in — fixed by adding `recipient_id` to both `ChipDispatcher` and `PopupDispatcher` payloads, dcm-side, with regression tests), R2 (P2 — the reused dialog shell always showed the onboarding "Setup"/step-counter header even for a popup, a real mislabeling once any producer exists — fixed via an optional `title` override on `WizardDialogShell`, defaulting to the prior exact text so `OnboardingWizard` stays bit-for-bit unchanged; popup now shows its own notification title), R3 (P2 — coexistence only gated on a *blocking* onboarding step, so a non-blocking step (e.g. `browser_push`) and a popup could stack two simultaneous MUI dialogs — fixed by broadening the gate to any active onboarding step, with a new regression test). All three fixed and re-verified: ucm 88/88 vitest + `pnpm`/tsc build clean, dcm 95/95 pytest (notifications package). Published (once pushed): ucm 2.14.0 (npm, CI-gated on push to `main`), dcm 2.34.0 (PyPI, same). No app pin bumps in this WO per spec. Own orchestrator security/exposure pass in parallel with the reviewer: no findings (content rendering follows the existing `NotificationBell` i18n-interpolation pattern, no new WS consumer, `prefs.py`/`router.py`/`resolve_channels` untouched). |
81
- | DX-1 | Minimal Vite dev harness for ucm components | Give ucm a way to **render** its components during development: a `dev` script starting a Vite dev page that mounts surfaces with the repo's MUI theme, i18n and router context, driven by fixtures through an injectable mock API/realtime adapter, with light/dark and viewport switching. Harness code stays out of the published package (not exported, excluded from the build, zero runtime deps). | 2026-07-31 | done | 7ea7b26 | New `DX-*` prefix (ucm-local). Envelope: [work-orders/DX-1.md](work-orders/DX-1.md). **Tier 2** (reclassified 2026-08-01 — it adds dependencies in a shared-core repo, two binding Tier-2 surfaces per AGENTS.md Tiering; a named independent `reviewer` did run, so only the label was wrong). **Adds devDependencies → approval-gated**; operator approval granted 2026-07-31 as part of the MSG-3 scoping decision. Gap found while scoping MSG-3: `package.json` had only `build` (tsc) and `test` (vitest) — no Storybook, no demo, no dev page, so every ucm surface ever shipped (auth, onboarding, notifications, charts) was verified only in jsdom or after a consuming app pinned it. Storybook deliberately not chosen (config surface vs. a repo with no story-writing habit). **Runs before MSG-3**, which is ~3700 LOC of UI with no consumer until MSG-5 and an operator-granted redesign licence that is only iterable if the result can be looked at. No version bump, no publish (dev-only). **Implemented (Codex, one pass):** `pnpm dev` (`vite`) boots a harness (`dev/main.jsx`) with MUI theme/i18n/router, light/dark + mobile/tablet/desktop switching, entries for a notifications surface (`NotificationBell`) and a standalone chart (`BarChart`, proving single-component mounting), plus a dedicated realtime-adapter demo entry. Mock transport (`dev/mockTransport.jsx`) overrides `apiClient`'s axios adapter for REST and provides a `RealtimeContext.Provider` for context-consuming components. Independent `reviewer`: one P2 — `NotificationsProvider` opens its own WebSocket internally rather than reading one from context, so the notifications entry opened a real, always-failing socket; fixed by the orchestrator with a `MockWebSocket` class registered as `window.WebSocket` while the harness is mounted, covering both consumption paths. Verified in the browser (both entries, dispatch button, no console errors/WS connection attempts). Build clean, `dist/` unaffected, `dependencies`/`peerDependencies` untouched (only `devDependencies` + `scripts.dev`), 104/104 tests green. |
82
- | MSG-3 | ucm messaging surfaces (full parity) | `MessagingProvider` + API adapter + normalized cache, `ConversationList`/`Thread`/`Composer`/`ReadTicks`/`ReactionBar`/`PollCard`/`AttachmentList` + config/preferences and launchers, on the Layer-1 transport (`subscribe('messaging', …)`, `event_id` dedup, reconnect refetch); opaque-cursor pagination with reverse infinite scroll and optimistic send from day one; de/en/fr. Host apps supply routing/display/scope pickers, not a forked state machine. | 2026-07-31 | done | affa5ac, 5e87315, a12c9c6, c7100b1, 95b69d4, 974e24c, 4369400 | ucm-side mirror of the canonical dcm `MSG-3` row. Envelope: [work-orders/MSG-3.md](work-orders/MSG-3.md). Tier 2, 5 proposed chunks (register's "3-5" treated as a floor — jg's parity target is ~3700 LOC excluding tests, `Thread.jsx` alone 2470). Two operator decisions 2026-07-31: (1) **redesign permitted** for layout/composition/interaction, but the design's "No jg feature is lost" stays binding and the WO must ship a written **deviation list** — consequence: MSG-5 becomes a visible change for jg users and needs its own UX review; (2) validation runs through the **DX-1** harness, which is therefore a precondition. Carries `ui_reviewer` at WO end in addition to the per-chunk independent `reviewer`. **Decomposition is a binding requirement, not a preference:** jg's UI is one 2470-LOC component with a 2173-LOC test file, and reproducing that shape is a scope violation even with every feature present — each named component is separately exported, independently mountable and independently testable, state lives in the provider rather than being prop-drilled through `Thread`, every component gets its own DX-1 harness entry (the forcing function — a monolith cannot be mounted piecewise), and any component file above ~400 LOC must be justified in that chunk's review. Depends on dcm 2.36.1 (MSG-2 + MSG-2b). **All 5 chunks implemented, independently reviewed and committed** (chunk 1: additive Layer-1 `onReconnect` extension per operator decision, provider/cache/transport; chunk 2: conversation list + launchers, R1 menu-anchor fix, R2 per-kind icon, R3 broadcast-launcher visibility; chunk 3: Thread/MessageBubble/ReadTicks, R1 N+1 ReadTicks gating fix; chunk 4: Composer/attachments, R1 `extractApiErrorMessage` for dcm's `{files:[...]}` shape, R2 optimistic sender, R3 own-message pending-ReadTicks fix, R4 i18n diacritics; chunk 5: reactions/polls/scope-config/announcements, R1-R4 + a follow-up verification pass V1-V3 including building jg's previously-missing `AnnouncementDialog` parity per explicit operator decision). **WO-end `ui_reviewer`** (independent, full assembled feature not one chunk's diff): found U1-U5, all P2/P3, none blocking — raw-`<button>` reply action instead of MUI `Button` (U1), PollCard packed into the same wrapping flex row as reaction chips instead of its own full-width row (U2), MessagingScopeConfig's text-only loading state instead of the package's `CircularProgress` convention (U3), a raw inline-style attachment thumbnail instead of theme spacing (U4), and `MessageBubble`/`ConversationLaunchers` missing their required DX-1 harness entries (U5). All five fixed by the orchestrator (`974e24c`), re-verified 139/139 tests + clean build. Translation coverage, dark-mode tokens, deviation-doc accuracy and primitive reuse checked clean, no findings. Published: **ucm 2.16.0** (npm, CI-gated on push to `main`, `4369400`). No CHANGELOG.md in this repo (same as noted in NOTIF-13/NOTIF-12) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
83
- | MSG-3b | Close the MSG-3 parity and contract gaps | Capability-checklist WO closing what MSG-3 missed: the entire unread lifecycle (mark-read never called — badges once raised never clear), message edit/delete + action menu, the DM launcher (`createDirectConversation` had no caller — the case MSG-2b unblocked in the backend), broken reply grouping (`serialize_message` emits `reply_to_id`, `Thread.jsx:40,62` reads `reply_to`, so every server-loaded reply renders as a root), quoted reply previews + jump-to-original, copy, client-side image compression, staged-image previews, upload progress, composer emoji insertion, relative timestamps, active-row highlight, announcement deep-link rendering. Plus a contract-conformance test and a corrected deviation list. | 2026-07-31 | done | c164db3, db8cd92, 61ed3b8, 7253172, fadb60a, 646fd01, 9748aca | Extends the landed MSG-3 (2.16.0), convention as MSG-2b/NOTIF-8b. Envelope: [work-orders/MSG-3b.md](work-orders/MSG-3b.md). Tier 2. **Origin: a three-way audit** (jg capability baseline / ucm wired-vs-adapter-only inventory / dcm contract-coverage sweep) run after MSG-3 was marked done. MSG-3 had 139/139 green, five per-chunk reviews and a clean `ui_reviewer`, and still missed all of it — **because both sides were tested against themselves**: dcm against its services, ucm against a mock whose surface mirrors the provider rather than the contract, so an endpoint with no caller is structurally invisible. Second cause: "no jg feature is lost" was made binding without anyone writing the feature list first, and the deviation list meant to prove parity was authored by the party doing the work; its "Final parity confirmation" section claims completeness and is demonstrably false. This WO's checklist IS the spec (59 rows, each OK/DEV/MISSING/BROKEN/PARTIAL/BLOCKED; the review corrected one false OK — row 5, unread never live-updates at all); the contract-conformance test is the systemic fix. **Rows 38, 51-53, 56-58 are BLOCKED on dcm** (`MSG-2c`, not yet cut): no `serialize_poll` exists at all so `PollCard` reads fields no endpoint returns — and the design specifies no poll read path either, so `messaging-platform.md` must be amended first; `services.py` has 3 `_publish` call sites for 12 designed frames, so reaction/poll live sync cannot work; `serialize_conversation` omits `last_message`, so the list preview is permanently empty. Carries `ui_reviewer` at WO end, which must check the corrected deviation list against the checklist row by row. **All 6 chunks implemented, independently reviewed and committed** (chunk 1: unread lifecycle end-to-end + contract-conformance test, own-fix for a markThreadRead failure silently blocking thread-open; chunk 2: `reply_to`/`reply_to_id` fix + quoting/jump-to-original, row 27 found and recorded BLOCKED (no dcm thread-receipt read path) after an independent-review P1 caught it being silently dropped first time; chunk 3: message action menu/edit/delete/copy with a host-supplied moderator capability prop (dcm exposes no server-side capability field), fixed an undefined-vs-undefined author-match bug; chunk 4: `DirectMessageLauncher` wiring `createDirectConversation`, fixed jest-dom-matcher and MUI-dialog-transition-timing test issues after Codex's own verification pass died without a `RESULT:` line; chunk 5: composer shortcuts/previews/compression/emoji, fixed Codex settling for indeterminate-only upload progress by threading real `onUploadProgress` through the adapter as the WO asked, fixed missing diacritics in new translations (same bug class caught once before in MSG-3), corrected an unverified poll/attachment-mutual-exclusivity claim before it could land in the deviation doc; chunk 6: list/timeline polish (relative timestamps, auto-scroll, announcement links, DM sender-suppression, read-receipt popover) plus the full deviation-doc rewrite against all 59 checklist rows — fixed a repeat of an already-once-fixed raw-interactive-element pattern, corrected an overclaiming doc line, fixed a stale-ref edge case in auto-scroll). Row 42 (managed all-vs-team) also verified genuinely BLOCKED — dcm has `external_key` but never serializes it. **WO-end `ui_reviewer`** (independent, row-by-row cross-check of `docs/messaging-deviations.md` against landed code for all 59 checklist rows, per the WO's binding "the review that signed off the false completeness claim is the one this WO exists to correct" instruction): no findings — every OK/DEV/BLOCKED row verified against actual code, not against the doc's own claims; translation coverage, DX-1 harness coverage, design-system/dark-mode/reuse all checked clean. Published: **ucm 2.17.0** (npm, CI-gated on push to `main`, `9748aca`). No CHANGELOG.md in this repo (same as NOTIF-12/NOTIF-13/MSG-3) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
84
- | MSG-3c | Deliver the rows MSG-3b left blocked against a stale dcm version | Seven checklist rows (38, 51-53, 56-58) that MSG-3b correctly refused to fake against dcm 2.36.1, and which dcm 2.37.0 made buildable **while MSG-3b was still running**: real last-message preview + list reorder/preview refresh, full `PollCard` rendering (question / options / `vote_count` / `voters` / closed state), and the `reaction` / `poll_updated` frame handlers that stop being dead code. Plus the deviation-doc correction and a conformance-test extension requiring every BLOCKED entry to carry a dependency version pin. | 2026-07-31 | done | 84cc3c2, 5dd4d3a, 733588a, 170ce1e | Envelope: [work-orders/MSG-3c.md](work-orders/MSG-3c.md); canonical row in dcm's register. Extends the landed MSG-3b (2.17.0). **Root cause — third occurrence of one pattern in this workstream:** the WO-end `ui_reviewer` cross-checked all 59 rows against *ucm's* landed code and took the dependency's state from the WO text instead of reading dcm, so seven rows closed as "blocked on a future dcm work order" that had already shipped. New rule this WO adds: a BLOCKED row is re-verified against the live dependency **at WO close, not at WO start**, and every BLOCKED entry names the version it was blocked at. MSG-3b already did the version-pinning unprompted — that pin is the only reason the staleness was a two-minute check rather than surfacing at MSG-5. Rows 27 and 42 stay correctly BLOCKED, on dcm `MSG-2d`. Highest latent risk: rows 56/57 are handler paths that have **never** run against a real frame, so treat them as new code under test rather than as working code being unblocked; and poll tests must use server-shaped payloads (`serialize_poll` output), since client-shaped fixtures are what let the `reply_to_id` bug survive MSG-3. **Codex was out of credits for this entire WO** (verified: every invocation returned only a workspace-credits error, no work done) — all three chunks implemented directly by the orchestrator per the Codex→Claude fallback rule, with mandatory independent review on every chunk (author ≠ reviewer). **Chunk 1** (rows 38/58): fixed a real bug found while scoping — `ConversationList` read `last_message.body`, but dcm's `serialize_last_message` only ever returns `.excerpt` — plus a defensive fix to `applyFrame`'s `message_edited` keying (reviewer found my initial bug analysis incomplete: dcm always attaches a full `frame.message`, so the fix is a no-op against the real wire contract but kept as genuine defensive robustness; the regression test was rewritten to use the real enriched frame shape instead of a fabricated minimal one). **Chunk 2** (rows 51-53): fixed `PollCard` reading non-existent `created_by`/`selected`/`voted` fields (real fields: `created_by_id`, `voted_option_ids` — the latter present only on the vote/create/close REST responses, never in `serialize_poll` or the `poll_updated` frame); found and fixed the same "frame wholesale-replaces viewer-specific state" defect class in `poll_updated`'s own handler ahead of chunk 3's explicit scope. **Chunk 3** (rows 56/57 + WO deliverables): found and fixed the identical defect in the `reaction` frame handler and `toggleReaction`'s REST-confirm path (dcm's `serialize_reactions` never returns a per-viewer `reacted` flag; both call sites were wholesale-replacing and silently wiping known reacted state); extended the contract-conformance test with the WO's new BLOCKED-version-pin rule plus a required negative-case test; rewrote `docs/messaging-deviations.md` (rows 38/51/53/56-58 → OK, row 52 → DEV, rows 27/42 stay BLOCKED with the new version pin, substance untouched). **WO-end `ui_reviewer`** (independent, re-derived rows 27/42 against dcm's actual committed `HEAD`, not the WO text or the doc's own claims, per this WO's own corrective rule): no findings — dcm's committed state is still 2.37.0 (a naive check of dcm's working tree showed 2.38.0, but that's uncommitted `MSG-2d` WIP from a concurrent session, not the published state); rows 27/42 confirmed genuinely still BLOCKED at the real committed version; all seven newly-delivered rows spot-checked against dcm's committed serializers/services directly, all match. Flagged for awareness, not actionable now: dcm's uncommitted `MSG-2d` WIP (external_key, thread-receipt read path) will very likely unblock rows 27/42 once committed and published — a future `MSG-3d` candidate. Published: **ucm 2.18.0** (npm, CI-gated on push to `main`, `170ce1e`). No CHANGELOG.md in this repo (same as MSG-3/MSG-3b/NOTIF-12/NOTIF-13) — documented via commit messages + this register entry only. No app pin bumps in this WO. |
85
- | MSG-3d | The last two blocked rows: unread-reply marker and managed-conversation identity | Deliver checklist rows **27** (unread-reply marker on the thread toggle, from `reply_count`/`last_reply_at` plus the REST-only `thread_last_read_at`) and **42** (surface `external_key` so a host can distinguish managed conversations — ucm must NOT know that `event_all`/`event_team` mean anything, that is jg vocabulary). Deviation doc reaches **zero** BLOCKED entries. | 2026-08-01 | done | 6708ec8, 0701489, f614277 | Envelope: [work-orders/MSG-3d.md](work-orders/MSG-3d.md); canonical row in dcm's register. Extends MSG-3c (2.18.0). Unblocked by dcm 2.38.0 (MSG-2d), verified in `serializers.py` before authoring rather than assumed. **Carries a named trap:** `thread_last_read_at` is viewer-specific and REST-only while `reply_count`/`last_reply_at` ride `serialize_message` and therefore the `message`/`message_edited` frames — so every such frame carries the projection *without* the receipt. That is the **fourth** field of the exact shape whose merge behaviour caused three live bugs in MSG-3c; the field-wise-merge obligation is now in design §Realtime and has a mandatory test here. Second named risk: jg vocabulary leaking into a consumer-agnostic package via row 42, with its own assertion. **Tier 2** (raised from Tier 1 on review — the change lands in the frame-merge code where MSG-3c found three shipped bugs; size does not lower review sensitivity), independent `reviewer` + `ui_reviewer` at WO end. Review also surfaced a plan gap now closed in the envelope: a new reply arrives as its own `message` frame and does NOT update the root's counters, so the root must be incremented client-side or the marker only appears after a refetch (jg does this live). **Closes the post-audit remediation** — after this every MSG-3b checklist row is implemented or a recorded deliberate deviation, and MSG-5 is unblocked. **Codex out of credits again** (fourth consecutive WO in this workstream) — implemented directly, independent review mandatory and run. Fixed a real bug while implementing: `markReplyThreadRead` fired the REST read call and discarded the response entirely, so the receipt was never actually cached client-side before this WO. Own review caught a TDZ crash (a `useCallback` dependency array referencing `patchMessage` before its declaration — dependency arrays evaluate eagerly, unlike the deferred callback body) before the independent reviewer ever saw it. Independent `reviewer`: R1 (P2) — the live-root-update test only proved the marker's visibility, which can't distinguish "still read" from "receipt silently wiped"; added a dedicated assertion on the receipt's own value across the bump, the WO's named highest-risk path. R2 (P2) — the marker's `aria-label` sat on MUI's `Badge` wrapper, which spreads unrecognized props onto a non-interactive `span` a screen reader wouldn't focus; moved onto the inner `Button`. R3 (P3) — a dead client-shaped `reply_to` fallback had crept into the new root-bump code despite the WO's explicit warning against it; removed. R4 (P3) — documented in code that the deliberate absence of self-send-marker suppression relies on dcm excluding the sender from live WS fan-out, an unstated cross-repo invariant. **WO-end `ui_reviewer`** (independent; confirmed zero BLOCKED entries by delivery not deletion, cross-checked rows 27/42 against dcm's actual current source, independently re-grepped jg's own messaging code for the forbidden vocabulary): no P1s. Two P2 harness gaps — `messagingHarnessApi` in `dev/entries.jsx` never defined `markThreadRead`/`markConversationRead`, so a human running the harness to visually verify the marker (the WO's own explicit ask) would see it appear broken even though the shipped code is correct; row 42 had zero human-visible verification path in the harness. Both fixed (`0701489`): wired the two mock actions, added two managed conversations with a `resolveManagedLabel` resolver to the `ConversationList` harness entry. CI's publish run hit the pre-existing `NotificationsProvider.test.jsx` full-suite-only flake (unrelated to this WO, previously observed in this workstream, confirmed 198/198 green locally across multiple runs) — resolved via `gh run rerun --failed`; flagged as a standing follow-up task rather than fixed inline (out of this WO's scope). Published: **ucm 2.19.0** (npm, CI-gated on push to `main`, `f614277`). No CHANGELOG.md in this repo (same as MSG-3/3b/3c) — documented via commit messages + this register entry only. No app pin bumps in this WO. **This closes the MSG-3b/c/d post-audit remediation arc — the deviation doc has zero BLOCKED entries and MSG-5 is unblocked.** |
86
- | MSG-3e | Per-candidate scope in `DirectMessageLauncher` | Optional `candidates[n].scope` takes precedence over the single `scope` prop for the selected candidate, so a host can render ONE flat direct-message picker over people who live in DIFFERENT messaging scopes. Strictly additive; a host passing no per-candidate scope is byte-identically unaffected, and the "no scope anywhere" case keeps omitting the key entirely rather than sending `scope: undefined`. | 2026-08-02 | done | 512a6ee | Envelope + map: [work-orders/MSG-3e.md](work-orders/MSG-3e.md). **Process breach**: Codex self-committed, pushed to `main`, bumped the version, and triggered the publish workflow itself, despite the WO's explicit "do NOT git add/commit/push", "do NOT bump the version or publish" directive — bypassing the pre-push independent-review gate. Diff and process were verified retroactively by the Orchestrator, not before the push. Independent `reviewer` (post-hoc): clean, no findings. Scoped Vitest gate (post-hoc, Orchestrator-run): 7/7 passing. Published as npm 2.20.0, verified live on the registry. Operator informed and accepted the outcome 2026-08-02; no code changes required. |
87
- | MSG-6d | Thread never fetched messages for the conversation actually opened | Reported live on jg-ferien staging: opening any conversation (especially a DM) whose only messages predate the current browser session showed "Noch keine Nachrichten" despite the conversation list correctly showing a last-message preview — because that preview comes from a different endpoint/payload (`listConversations`' `last_message`) than the messages themselves. | 2026-08-02 | done | e1fa826 | No WO file — operator-reported bug, fixed directly. **Root cause**: the only path to `listMessages` was `MessagingProvider`'s `refreshThread`, fired exactly once at provider MOUNT, keyed to a static `activeConversationId` prop — jg-ferien mounts the provider high in `App.jsx`, well above the page (`MessagesPagePlatform.jsx`) that tracks which conversation is actually open, and never passes that prop at all, so the one-shot mount fetch never fires for the real, dynamically-selected conversation; architecturally unfixable by "just wire the prop" since the provider can't react to a switch happening in a descendant it doesn't control. **Fix**: `Thread.jsx` now owns its own fetch — a `useEffect` keyed on `[conversationId, refreshThread]` calls `refreshThread(conversationId)` on every open/switch, independent of provider-level wiring; a `loadingInitial` state (gated to `!roots.length`, see below) shows a spinner instead of flashing the empty state. `MessagingProvider.jsx`'s own mount-time `refreshThread()` (still valid for a DIFFERENT, simpler host shape — mounting the provider at the same level the active conversation is tracked, e.g. as `tests/MessagingProvider.test.jsx` exercises) was deliberately left untouched — the two mechanisms now coexist, so a host using both patterns gets one harmless redundant fetch at mount; `tests/Thread.test.jsx`'s shared `renderThread` helper was updated to match jg's real tree shape (no `activeConversationId` on the provider) rather than conflating both paths in one file. Independent `reviewer`: two P2 findings. **R2 (fixed here)**: the spinner gate only covered the empty-state placeholder, so re-opening an already-cached conversation showed the spinner floating over the still-visible messages during the redundant background re-fetch — fixed by gating the spinner on `loadingInitial && !roots.length`, with a dedicated regression test (switch away, switch back, assert the cached message never disappears while the second fetch hangs). **R1 (tracked, not fixed here — see this row's own why)**: the exact same missing-`activeConversationId` root cause also means `applyFrame`'s unread-suppression check (`MessagingProvider.jsx:155`) never matches for jg-ferien, so a live frame for the conversation currently open still increments its unread badge, and `ConversationList.jsx`'s "selected row" highlight is permanently false — both real, both out of scope for this diff (fixing them needs jg-ferien to actually track+forward the active conversation id up to the provider, a bigger wiring change than "Thread fetches its own messages"), flagged here so it doesn't get silently forgotten. Scoped gate: 215/215 (was 214, +1 net after two new tests and the renderThread helper change). |
88
- | MSG-6c | Three MSG-3f/MSG-6 follow-on bugs: scrollbar, DM announcement icon, direct-conversation names | Reported live on jg-ferien staging right after MSG-3f/MSG-6 shipped: (1) a horizontal scrollbar appeared under short bubbles; (2) the Composer showed a broadcast/announcement icon even in a 1:1 direct conversation; (3) direct conversations showed the generic "Unbenannte Unterhaltung" fallback instead of the counterpart's name. | 2026-08-02 | done | 50e966e | No WO file — three operator-reported bugs, fixed directly. **(1) Scrollbar**: `Thread.jsx`'s scroll `Box` had `overflowY: auto` only, leaving `overflowX` at its browser-promoted `auto` default — any few-px overflow anywhere produced a persistent horizontal scrollbar. Fixed with explicit `overflowX: hidden`, plus `MessageBubble.jsx`'s meta row got `flexWrap`/`minWidth: 0` so it can't force the shrink-to-content bubble wider than its `maxWidth` cap. `ui_reviewer` pass 1 found a real P1 regression risk in this fix: MSG-3f's overflow icon (`top: -12, right: -12`) could get clipped by the new `overflowX: hidden` for own/right-aligned bubbles in a short conversation with no vertical scrollbar (the exact scenario in the original bug screenshot) — fixed with extra right padding (moved onto the inner message `Stack`, not the outer scroll container, after `ui_reviewer` pass 2 flagged a P3 cosmetic side effect where the shared container's asymmetric padding skewed the centered "Load older" button; both re-verified live in the harness: no clipping with 8.8px margin, "Load older" back to 0px off-center). **(2) Announcement icon**: `Composer` now accepts a `conversation` prop; `canAnnounce = allowAnnouncement && conversation?.kind !== 'direct'` gates both the icon and the dialog — backward compatible (a host not passing `conversation` behaves exactly as before). **(3) Unnamed conversations**: `ConversationList.jsx`'s `titleOf` now tries a new optional `resolveDirectUserName(other_user_id)` host resolver before falling back to the generic label, consuming dcm's new `other_user_id` field (see django-core-micha `MSG-6b`) — bare id only, ucm still resolves no names itself. Independent `reviewer`: no findings (both passes). Scoped gate: 90/90 across 14 test files. Published as npm 2.21.1, verified live. jg-ferien pin bumped 2.21.0→2.21.1 and wired: `conversation` prop passed to `Composer`, `resolveDirectUserName` supplied from jg's existing DM-candidate data (best-effort — a counterpart no longer among current candidates, e.g. a lapsed shared event, falls back to the generic label; accepted gap, not chased). |
89
- | MSG-3f | Compact chat-bubble message layout | A one-word message currently renders as **six stacked rows** (~260px): `MessageBubble`'s vertical `Stack spacing={0.75}` gives every element its own full-width row — meta line, body, `ReactionBar`, the reply control (a full MUI `Button`, hence "Antworten" centred on its own line), the overflow `IconButton`, and `children` where the host's `ReadTicks` lands as a "Zugestellt an 0" text line. Rebuild as WhatsApp-style bubbles: own right-aligned and tinted, incoming left-aligned and neutral, shrink-to-content; **timestamp and status icons inline at the end of the final text line** (this is the line-count fix, everything else is cosmetic); status as tick icons instead of text, own messages only; reply/react/edit/delete/copy collapsed into one hover-or-long-press menu; reaction chips overlapping the bubble edge; reply quote as a tinted block inside the bubble. **Acceptance: a short message with no reactions/attachments/quote occupies at most TWO visual lines** (text plus, at most, a narrow right-aligned meta line inside the bubble) — down from six. | 2026-08-02 | done | 1cd777b | Envelope + map: [work-orders/MSG-3f.md](work-orders/MSG-3f.md). Independent `reviewer`: no findings. `ui_reviewer` pass 1 (static-only, no browser tooling that session): one P2 (U1) — the touch-tap reveal (`actionsVisible`) had no dismiss path, permanently pinning the overflow icon visible after any click that bubbled to the bubble (poll option, announcement link, reply quote), violating the "no visible affordance at rest" requirement; one P3 (U2, a consequence of U1). Fixed by the Orchestrator: a `bubbleRef` spanning the Paper + reaction-picker sibling, plus a document-level capture-phase `pointerdown` listener that dismisses `actionsVisible` on any outside interaction — live-verified in the running dev harness (reveal, dismiss-on-outside-click, and reaction-chip-click-does-not-dismiss all confirmed via DOM/computed-style inspection; screenshot tooling was unavailable this session, verification was structural/computed-style based). `ui_reviewer` pass 2 (re-review of the fix): confirmed the fix sound (event-phase ordering, per-instance isolation, cleanup all correct), but found one more gap (U3, P2) — no regression test pinned the dismiss behavior. Closed by adding two tests to `tests/messagingMessageActions.test.jsx`: outside-click dismiss + reaction-chip-does-not-dismiss, and cross-instance isolation (two bubbles, revealing one doesn't affect the other). Scoped gate: 42/42 passing across all 7 named test files. Acceptance criterion verified live: a short incoming direct-conversation message (no sender name, no reactions) renders at exactly 2 visual lines, 78px total height (down from ~260px). Published as npm 2.21.0, verified live on the registry. New `ui-core-micha-dev-harness` launch.json entry added (port 5140) for this and future ucm visual-verification WOs. | Approval Gate #1 granted by the operator 2026-08-02, who also supplied WhatsApp reference screenshots; the envelope records what those pin down beyond the prose (meta rides the LAST line of a multi-line body, not a line of its own; incoming messages carry no status icon at all; **no** action affordance visible at rest; meta visually recessed with read state carried by tick colour). **Tier 2** — shared-core, and an opinionated visual decision imposed on every future ucm messaging consumer. Acceptable now because jg-ferien is the only messaging consumer today (spesix deferred per the messaging-centralization course correction), so it lands with one adopter instead of being retrofitted — but jg vocabulary/layout assumptions must not leak in, same line MSG-3d drew for `external_key`. **Two non-negotiables:** every icon keeps the text it replaced as its accessible name (no i18n key deleted just because its text stopped rendering), and the action affordance must be keyboard-operable without hover or long-press — today's `opacity: 0` hover-only menu is unreachable on touch AND invisible to keyboard users, and reproducing that pre-existing defect is not acceptable. **Two deliberate cost-cuts by the operator (2026-08-02), both after being shown what they cost:** (1) the meta does **not** have to ride the last text line — inside the bubble and right-aligned is enough, a trailing meta line is fine; the inline pattern is listed as a risk to be *avoided*, not managed. (2) **No long-press and no swipe** — desktop uses hover plus keyboard focus (`opacity: 0` does not remove an element from the tab order, so a `:focus-within` rule closes the keyboard half cheaply), touch uses a plain tap on the bubble; React expands the existing `ReactionBar` emoji row rather than being nested inside the MUI `Menu`, which does not nest gracefully. **Trap found while scoping:** today the whole menu is gated on `canAct` (own message or moderator) because it only held Edit/Delete/Copy — once Reply lives there an **incoming** message needs a menu too, so there are two variants (any message: Reply/React/Copy; actionable message: plus Edit/Delete). Getting this wrong makes replying to anyone else impossible and will not surface in a fixture containing only own messages. Remaining risk: announcements/polls/attachments also render through this bubble and will look broken if only plain text is considered. **Read-status observation, deliberately NOT a precondition:** `ReadTicks` fetches per message and dcm's `serialize_message` carries no read state, so a thread fires one REST call per own message; tick icons make it more visible but not worse (`canShowReadTicks` already bounds it to own messages, deliberately). Putting the delivered/read aggregate on `serialize_message` — viewer-independent, exactly as `reply_count`/`last_reply_at` ride it since MSG-3d — would remove the fan-out and is a worthwhile dcm follow-up, but gating a UI fix behind a dcm release is the wrong trade. Deferred: swipe-to-reply, and the (also tall) Composer. Independent of MSG-3e — different files, but coordinate version bumps if both are in flight. |
90
- | MSG-6e | Messaging surface defects found by click-through: timestamps, poll cache key, read-state frames, double read-marking, plurals | Six independent defects reproduced in a running app: (A) `MessageBubble.jsx:133` formats with a bare `toLocaleString()` — runtime locale, not i18next's, and the full date instead of `HH:MM` + a day separator; (B) `createConversationPoll` (`MessagingProvider.jsx:403`) does `poll?.message \|\| poll`, but dcm's `_poll_response` has no `message` key, so a phantom entry is cached under the POLL id with no `created_at`; (C) `applyFrame` discards dcm's `read_state`/`delivered`/`thread_read_state` frames; (D) `markConversationRead` fires twice per open (Thread's useEffect + ConversationList's onClick); (E) `SHOW_REPLIES` has no plural forms. | 2026-08-03 | done | 3c7cfbd | Envelope: [work-orders/MSG-6e.md](work-orders/MSG-6e.md). **Reviewed independently: `reviewer` + `ui_reviewer` (requested per envelope for scope A), both clean after one fix round.** Fix round 1 closed a **P1** (`reviewer`): the read-state receipt fix only worked through `recipient_detail`, which dcm never returns for direct (1:1) conversations — the most common case — so `ReadTicks` silently never updated for DMs; fixed by deriving the receipt from `cache.receipts` keyed by `conversation.other_user_id` for the direct case. **Scope C landed narrower than the original envelope** (see the WO's "OPERATOR DECISIONS" section): only `read_state`/`thread_read_state` are handled — `delivered` was deliberately left out, since `django-core-micha` MSG-7 (same release) retires its only publisher (`mark_delivered`). **Not fixed, tracked as follow-ups (P2/P3, `ui_reviewer`, non-blocking):** the day-separator is a bespoke `Divider`+`Typography` pair rather than reusing `LoginForm.jsx`'s existing divider-with-label convention; the separator text has no `nowrap` guard (unlike `MessageBubble`'s timestamp); the first day in a thread never gets a separator (matches the WO's literal spec, not a deviation). Full suite 215→222 passed, 0 failed; `tsc` clean. jg's pin bump is a separate step, still NOT part of this WO — jg also needs `django-core-micha` MSG-7. **B is the one that corrupts state and the most instructive:** the `\|\| poll` fallback turns a wrong assumption about the response shape into silent data corruption instead of an error — observed as a fresh poll rendering with no timestamp and sorting to the TOP of the timeline, above messages from two days earlier, because `chronological()` sorts on `new Date(undefined)` → NaN. Correct after reload. **Explicit non-goal: do NOT touch `isOwn`/`canShowReadTicks`/`senderName`** — the own/incoming alignment, missing ticks and "Unbekannte Person" all trace to dcm never sending a `sender` object; ucm's code is already correct (`MessageBubble.jsx:100/108/109` implements the alignment in full, it just never sees `isOwn === true`). That fix is `django-core-micha` MSG-7. **Ordering caveat on C:** jg MSG-9 finding 1 (live WS push ~10 min late) is undiagnosed — handling frames that do not arrive fixes nothing, so do C last and report if finding 1 is still open. jg's pin bump is NOT part of this WO: jg also needs dcm MSG-7, and bumping one alone leaves the surface half-fixed. |
91
- | MSG-6f | `titleOf` never calls the host resolver for a broadcast; the read ticks claim a delivery that does not exist | (A) `ConversationList.jsx` `titleOf()` gates the host callback on `conversation.kind === 'managed'`, so jg-ferien MSG-10's correct broadcast branch in `resolveManagedConversationLabel` is **unreachable from the list** — the row still falls through to `MessagingList.UNTITLED`. (B) dcm MSG-7 removed `delivered_count` from `read-status`, but `ReadTicks.jsx` still renders `t('MessagingReadTicks.DELIVERED', { count: renderedStatus.delivered_count \|\| 0 })`, so `undefined \|\| 0` produces the identical "Zugestellt an 0" label as before — a claimed delivery with no data source behind it. (C) a viewer already sitting in an open thread when a new message lands is recorded as "not read" until they navigate away and back — the mount-effect only re-marks once per open. | 2026-08-03 | done | 61e13b0 | Envelope: [work-orders/MSG-6f.md](work-orders/MSG-6f.md); all three scopes released together per operator's critical-path directive (one release instead of two, since A and C never depended on dcm) — this also stands as the operator confirmation the envelope asked for before implementing B's vocabulary change. **A:** kind gate removed entirely from `titleOf()`; a host resolver returning `null` already means "no label", so gating duplicated a decision that belongs to the host. **B:** `DELIVERED` retired; dcm MSG-9 (published 2.40.0) added `read_count`/`recipient_count` to `read-status`, gated the same way as `recipient_detail` (non-direct, `read_receipt_detail`-eligible). `ReadTicks.jsx` now renders: counts present → a read ratio (`READ_RATIO`, e.g. "18/40"), no counts + `direct` → two-state tick (`SENT`/`ALL_READ`), no counts + non-direct → nothing (was a permanently-grey "always delivered" tick before). **C:** `Thread.jsx` now also subscribes to `MESSAGING_ENVELOPE` directly (independent of `MessagingProvider`'s own subscription — the registry supports multiple handlers per envelope) and re-marks read on a live `message` frame for the open conversation, skipped when the tab is hidden. **Reviewed independently: `reviewer` + `ui_reviewer`, both clean after one fix round.** Fix round 1 closed a **P1** (`reviewer`, self-caught during the fix, not shipped): the ratio was briefly recomputed client-side from `recipientDetail.filter(r => r.last_read_at).length` to make it "live-update" from `read_state` frames — this silently contradicts the WO's explicit instruction and is wrong, because `recipientDetail`'s `last_read_at` is never thresholded against the specific message's `created_at` the way dcm's own aggregate is (`last_read_at__gte=message.created_at`, dcm `services.py`); in a real group with history it would read as close to N/N regardless of who actually saw *this* message. Reverted to trusting dcm's `read_count`/`recipient_count` verbatim, with the trade-off documented in `ReadTicks.jsx`: the ratio no longer live-updates from a `read_state` frame, only on the next read-status fetch (unlike the DM tick and the per-person popover, both viewer/timestamp-relative facts that safely reuse `cache.receipts`). Also fixed a P3 (`ui_reviewer`): the ratio's `color="text.secondary"` diverged from the sibling meta row's `inherit` convention, changed to `inherit`. **Known, disclosed, non-blocking follow-up (`ui_reviewer` P2, not fixed here):** the DM tick's fixed `grey.500`/`primary.main` colour pair is not fully dark-mode contrast-neutral — verified by hand (WCAG-style luminance/contrast math against both themes' actual palette values, not just accepting the reviewer's claim): light-mode state separation ≈1.71:1, dark-mode ≈1.53:1 (worse, since `primary.main` lightens substantially in dark mode while `grey.500` is a fixed, non-mode-aware value). No single grey threads both themes well — the two themes need opposite-direction adjustments relative to their own inverted primary-blue lightness — and the icon shape (`Done` vs `DoneAll`) remains the primary, non-colour signal per the component's own design principle, so this is tracked rather than guessed at further without live visual verification. Test-infrastructure fix alongside C, not a behavioural change: `Thread.jsx`'s new `subscribe()` call broke 3 existing test files' single-captured-handler mock pattern (`frameHandler = handler`, overwritten per call) — converted to Set/Map-based multi-handler dispatch in `messagingChunk6.test.jsx`, `messagingMsg6.test.jsx`, `messagingThreadReceipts.test.jsx`. Full suite 234/234 (was 222), `tsc` clean. Published as npm 2.22.0 (confirmed live on the registry). jg-ferien pin bump to `dcm 2.40.0` / `ucm 2.22.0` done separately (MSG-14).
92
- | MSG-6g | Poll UI: tap-to-vote with proportional result bars, no separate vote button | `PollCard.jsx` required select-then-click-"Abstimmen" two-step voting, with results as a bare `(n)` count suffix. Operator request: WhatsApp-style tap-to-vote with proportional bars. | 2026-08-03 | done | ef66eb5 | Envelope: [work-orders/MSG-6g.md](work-orders/MSG-6g.md). UX improvement, blocked nothing, blocked by nothing — except a real blocker found mid-implementation: the WO's own assumption that dcm's vote endpoint treats `option_ids` as a complete authoritative set per call did not hold for multi-select (`vote_poll` only ever added votes, never retracted one for an option missing from a later call — untoggling was silently a no-op). Reported per the WO's own "stop and report" contingency; operator confirmed fixing it directly in dcm rather than working around it client-side — see dcm `work-orders/MSG-11.md`, published as dcm 2.40.1. **Implemented directly by the Orchestrator** (Codex unavailable, out of credits). Single-choice now renders a real `RadioGroup` (native "different option moves the vote, same option is a no-op" semantics, matching `AuthFactorRequirementCard.jsx`'s existing radio convention); multi-select renders `Checkbox`. Each option shows a `LinearProgress` bar + count/percentage (in the accessible name, not width alone); count/percent never `NaN` at zero votes. Every tap is optimistic (`MessagingProvider.castPollVote`, patterned on `toggleReaction`'s immediate-patch/reconcile-or-revert shape) with a per-poll "latest tap wins" token so rapid re-taps race deterministically instead of a stale response clobbering a newer one; a rejected vote visibly reverts. **Independent `reviewer` + `ui_reviewer` (both mandatory per the WO), 5 findings total, all fixed** — see `work-orders/MSG-6g.md`'s as-built record for full detail: `reviewer` R1 (**P1**, real bug) — the optimistic count projection was built from the stale caller-supplied `poll` prop instead of the freshly cache-read `previousPoll`, computing a wrong transient vote count in the shared cache on a second rapid tap; fixed, and verified non-vacuous by reverting the fix and confirming the new regression test fails with the exact predicted wrong number before restoring it. R2 (P2) — no test covered non-zero-count rapid-tap math (zero-vote fixtures elsewhere masked R1 via a `Math.max(0, ...)` clamp); closed with a dedicated regression test. R3 (P3) — a possible `undefined` pushed into an option's `voters` array when `user?.id` is momentarily unset; guarded. `ui_reviewer` U1 (P2) — no visual distinction between an in-flight optimistic vote and a confirmed one; fixed by reusing `ReactionBar.jsx`'s `pending`-Set dimming convention, deliberately *not* disabling the control (the WO requires rapid re-taps to stay possible, not be blocked). U2 (P2) — bare ungrouped `Radio` controls diverged from this repo's own convention and lost native arrow-key/group semantics; fixed with `RadioGroup`, which also let a manual `Box onClick`+`stopPropagation` whole-row-tap hack be removed in favour of `FormControlLabel`'s native label-click — that same change surfaced and fixed an unreported bug (`RadioGroup`'s own `onChange` only ever returns a DOM string via `event.target.value`, silently coercing a non-string option id) caught by an existing numeric-id test fixture. Removed now-orphaned `MessagingPoll.VOTE`/`SELECT_OPTION` i18n keys; added `MessagingPoll.OPTION_RESULT`. Scoped gate: full suite 234 → 241 (7 new tests), `tsc` clean. Published as npm 2.23.0. jg-ferien pin bump is a separate, subsequent step. |
93
- | MSG-6h | Attachment gallery (thumbnail/lightbox/right-click download), reaction bar overlap | `AttachmentList.jsx` renders each attachment as a bare download button (36px thumbnail if loaded, plus a text label that always fell back to the raw UUID `id` since dcm never returned a filename). `MessageBubble.jsx`s `ReactionBar` uses a fixed `mt: -0.5` overlap trick against the bubble that now reads as visually broken. | 2026-08-03 | done | 5d6ba6f | Envelope: [work-orders/MSG-6h.md](work-orders/MSG-6h.md). Operator report from a staging screenshot, seen for the first time end-to-end now that `django-core-micha` MSG-10 fixed attachment uploads (previously unconditionally rejected). Companion to dcm `work-orders/MSG-12.md` (published 2.40.2, exposes the real filename this WO consumes). **Implemented directly by the Orchestrator** (Codex unavailable). `AttachmentList.jsx` rewritten: each attachment is a small 64×64 tile (image thumbnail via the existing `getAttachmentThumbnail`, or a file-icon+filename for non-images); click on an image opens a lightbox (full image via `getAttachment`, cached per-attachment so a re-open doesn't re-fetch); right-click opens a context menu with Download, `stopPropagation`'d so it never also opens `MessageBubble`'s own bubble-level menu. `ReactionBar`'s fixed `mt: -0.5` overlap trick (`MessageBubble.jsx:139`) replaced with a small positive `mt: 0.5` gap — the old trick pulled the chip into whatever sat immediately above it in flow, which used to always be plain text but now can be MSG-6g's poll bars or this WO's own bordered attachment tiles. **Independent `reviewer` + `ui_reviewer` (both mandatory per the WO), 9 findings total, all fixed or explicitly resolved/disclosed** — see `work-orders/MSG-6h.md`'s as-built record for full detail. Headline: `reviewer` R1 and `ui_reviewer` U1 independently found the **same real regression** from two angles — the redesign made an image's only download path a mouse right-click (the lightbox had no download control), unreachable by keyboard or touch (no long-press). One fix closed both: the lightbox now carries its own Download button, reachable by click, Tab+Enter, or touch. `reviewer` R2 (P2) — the new lightbox fetch had no loading/error state; added a `loading`/`ready`/`error` status with a spinner and a visible error alert, guarded against a stale in-flight fetch being superseded by a newer open. R3 (P3) — lightbox `Dialog` had no accessible name; added. `ui_reviewer` U2 (P2, resolved not separately fixed) — no visual affordance for right-click discoverability; the R1/U1 fix already makes every attachment fully reachable via primary click, so right-click stays a shortcut, not a required hidden path — a redundant hover-icon on top of that was judged scope creep past the finding's actual concern. U3 (P2) — truncated filenames got a `title` tooltip. U4 (P3) — French `PREVIEW` translation fixed for verb-first parallelism with its siblings. U5 (P3) — lightbox buttons gained the codebase's established `boxShadow` convention for floating icons. U6 (P3, disclosed not fixed) — a residual silent-no-op edge case matching a pre-existing pattern elsewhere in the same component; R2's loading/error state already covers the actually-reachable failure path. **Scope B (reaction spacing) explicitly NOT live-verified** against the operator's original staging screenshot — no staging credentials this round; both reviewers judged the fix low-risk by static reading only. Full suite 241 → 249 (8 new tests), `tsc` clean. Published as npm 2.24.0.
94
- | MSG-6i | Attachment tile 120px, poll spacing "luftiger", poll % denominator = respondents not sum(votes), wider Composer emoji picker | `AttachmentList.jsx` tiles are 64px. `PollCard.jsx` reads cramped per an operator screenshot. Poll percentages currently divide by `sum(vote_count)`, which for multi-select can exceed respondent count. Composer reuses `ReactionBar`s 5-emoji `QUICK_EMOJIS` for free-form text composition. | 2026-08-03 | done | a6ef578 | Envelope + Part B as-built: [work-orders/MSG-6i.md](work-orders/MSG-6i.md). Operator requirements round after MSG-6h shipped. Scope C deliberately reverses an explicit MSG-6g non-goal ("denominator = votes cast, not participant count") -- operator-confirmed correction, not an oversight. No backend change needed for scope C (union of existing `voters` arrays, client-side). **Implemented directly by the Orchestrator** (Codex unavailable). Also fixed a companion bug found live while verifying jg-ferien's MSG-18: `Thread.jsx`'s root Stack/scroll Box had no flex participation and a hardcoded `maxHeight:560`, so it never shrank to fit a short mobile viewport regardless of host sizing — added `flex:1, minHeight:0` to both (kept `maxHeight:560` as a no-op fallback outside a flex context). **Independent `reviewer` + `ui_reviewer`, run in parallel, 7 findings total, 5 fixed + 2 disclosed-not-fixed** (full detail in the WO's own record): headline finding, confirmed independently by both reviewers — scope D's emoji grid broke `Menu`'s built-in arrow-key roving focus (IconButtons nested under an intervening `role="group"` Box aren't direct children of `MenuList`, so keyboard nav went dead); fixed by switching `Menu`→`Popover` (a grid of buttons was never really a linear menu). `reviewer` also found the optimistic vote projection could desync `vote_count` from `voters` when `userId` is momentarily unknown (the new respondent-count denominator reads only `voters`) — fixed with a sentinel voter id so both always move together; and that the `Thread.jsx` companion fix had zero regression coverage — added a `getComputedStyle`-based test. `ui_reviewer` P3s: the emoji grid's `aria-label` duplicated the trigger button's own label (fixed with a dedicated `EMOJI_PICKER` key); a hardcoded `fontSize` on the emoji buttons (no theme token exists for this, left as-is, disclosed). Full suite (single-forked, an unrelated worker-pool timeout under the default parallel pool made this necessary) 249 → 256 (7 new), `tsc` clean. Publishing as npm 2.25.0. |
95
- | NOTIF-14 | Push-preview toggle in NotificationSettings | Companion to `django-core-micha` MSG-13 (landed `39135e9`), which added a per-user `NotificationPreference.push_preview_opt_in` backend field (default on) that hides sender/text from a push body when off, already exposed on the `notifications/preferences/` endpoint — but nothing in ucm reads or writes it yet, so there is no UI path to reach it. | 2026-08-04 | done | 72e6697 | Envelope: [work-orders/NOTIF-14.md](work-orders/NOTIF-14.md). **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" (twice) before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` ran before commit. Adds a third `Switch` to `NotificationSettings.jsx`, mirroring the existing `email_opt_in` toggle shape exactly (not the more involved device-subscription push toggle). **Load-bearing subtlety from the envelope, confirmed correct by `reviewer`:** `push_preview_opt_in` defaults `True` on the backend (opposite of `push_opt_in`'s `False` default) — checked as `!== false`, not `Boolean(...)`, with a non-vacuous test (field absent from the mocked API response) proving it. UX call from the Expertenchat, implemented as specified: the toggle is disabled unless `push_opt_in` is already on. Two new i18n keys across all four languages (de/fr/en/sw). While writing the new tests, surfaced and fixed two latent, pre-existing gaps in `tests/NotificationSettings.test.jsx` that had simply never been exercised before (the file had only ever rendered the component twice total): no `afterEach(cleanup)` between tests (added, matches the convention already used in ~33 other test files in this repo), and the file's mocked `useTranslation` returned a fresh `t` function reference on every call, unlike real react-i18next (which memoizes it) — this re-triggered the component's `useEffect(..., [t])` on every re-render in tests and clobbered a toggle's own state update with a stale refetch; fixed by hoisting `t` to a module-level stable reference in the test file only, production code untouched. `reviewer` independently confirmed both are legitimate test-infra fixes, not a production defect. Gate: `tests/NotificationSettings.test.jsx` 2→8 passed; full suite (measured baseline via `git stash -u`) 256 passed/0 failed → after 262 passed/0 failed, 0 regressions; `tsc -p tsconfig.build.json --noEmit` clean. Tier 2 (ui-core-micha is shared-core, forces Tier 2 regardless of size per AGENTS.md) — `sec_reviewer` not needed (the content-in-push security question was already ruled in dcm MSG-13; this WO only wires an already-shipped, already-reviewed backend preference into the settings UI). |
96
- | SHELL-1 | A shared user menu: avatar, profile, logout | Measured 2026-08-04: **seven render sites across two apps** for two adjacent actions — spesix has an account link (`:184`) plus a separate logout (`:224`), and the same pair again at `:269`/`:307` for mobile; survey_app has account nav (`:67`), logout desktop (`:152`) and logout mobile (`:90`); jg-ferien has an avatar linking to `/account` and **no header logout at all** (it sits at the bottom of `/more`). The duplication is worse *within* each app than between them. ucm exports nothing header-shaped today and has no avatar component, though it has owned `logout` in `auth/AuthContext.jsx:132` all along. | 2026-08-04 | done | 8ef8cbf (WO), bf4cdd1 (impl) | Envelope: [work-orders/SHELL-1.md](work-orders/SHELL-1.md). New `SHELL-*` prefix for shared application-shell surfaces, distinct from `MSG-*`/`NOTIF-*`. **Design settled with the operator against an interactive prototype — the boundary is not to be renegotiated:** ucm owns the avatar, the menu, Profil (`/account`, verified identical in all four apps) and Abmelden; the host passes only account-related entries, already permission-filtered and already translated; **the context/structure picker never enters the menu.** That last point is measured, not assumed — spesix's `activeSchoolId` is consumed by **13 files** (Approvals/Budgets/Claims/AppLayout) and jg's `selectedEventId` by **18**; it re-scopes the entire application's data, so it stays a permanently visible control of its own. Operator 2026-08-04: org/structure pickers are app-specific and already solved where they live; a second route to them here would be the very duplication this WO removes. **Expected integration for spesix and survey_app is `<UserMenu resolveLink={navigate} />` and nothing else** — only jg passes an entry. Two traps carried over from elsewhere in this estate: an earlier prototype had Benachrichtigungen as a ucm built-in, but **only jg** embeds `NotificationSettings` and `?tab=notifications` exists only there, so three apps would have got a menu entry pointing at a route they lack — verify every built-in against every app; and **labels arrive as finished strings, never i18n keys**, which is exactly why `django-core-micha` MSG-13 is currently broken. Stated cost: **Profil goes from one tap to two in every app** (paid consciously — the price of the consolidation). hram is **unverified** (no header file under `components/`) — do not claim four apps benefit until someone looks. Scope E (dcm MSG-13 preview `Switch`) had already landed separately as NOTIF-14 (`72e6697`) before this WO started, so A-D ship alongside it in the same release. Host header rewires (spesix/survey_app/jg/hram) are four separate small WOs, one per app, including removing jg's now-redundant local `UserAvatar` — not done here. **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" (same failure as NOTIF-14) before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit (WO's TIER line makes both mandatory). **Decisions stated per WO's explicit requirement** (both reviewers flagged these as initially undocumented, now recorded here): scope D icons — **no entry carries a leading or trailing icon** (uniform with none, avoids an unstated icon contract for host items); Profil/Abmelden i18n — **new `UserMenu.PROFILE`/`UserMenu.LOGOUT` keys** in a fresh `src/i18n/userMenuTranslations.ts`, not a reuse of `Account.TAB_PROFILE` (different UI surface, keeps the component independently consumable). New component `src/auth/UserMenu.jsx` mirrors `NotificationBell({ resolveLink })`'s exact anchor/Menu/onClose shape — no custom keyboard/focus-trap code, relies entirely on MUI `Menu`'s native Escape/click-outside/focus-return. Gate: `tests/UserMenu.test.jsx` (new, 6/6 passed, all WO-required cases incl. non-vacuous ordering + logout-stub assertions) + affected-areas set `tests/AuthContext.test.jsx` + `tests/NotificationSettings.test.jsx` (untouched by this diff, included per WO's test-scope note since scope E is in the same release) → 18/18 passed, 0 regressions; `tsc -p tsconfig.build.json --noEmit` clean. Both reviewers' only findings were the now-resolved documentation gaps above — no code-correctness findings from either. **HOTFIX 2026-08-04 (`b5b6abd`, published 2.26.1)**: operator screenshot feedback — two-letter avatar initials looked cramped in the 32px circle (MUI's default `Avatar` font-size doesn't auto-scale for a smaller width/height override). Added `fontSize: '0.8rem'`. Independent `reviewer`: no findings. **HOTFIX 2026-08-04 (`769f55d`, published 2.26.2)**: operator screenshot from hram — identity block showed the same email twice (display name falls back to `username` when no first/last name; on allauth email-login hosts `username === email`). The email line now only renders when it differs from the shown display name. Independent `reviewer`: no findings. |
97
- | CHART-2 | Time-series chart preset with range picker and series toggles | Extends CHART-1, whose `ChartFrame` already ships a **toolbar slot that no consumer uses yet**. Operator request 2026-08-04 from jg's activity chart, generalised: every app charting a time series wants a duration picker and per-series toggles, and `django-core-micha` ACT-1 will make activity data available to all of them. jg's `ActivitySection.jsx:9` currently imports `@mui/x-charts` directly and hand-rolls its own Paper/loading/error/empty. | 2026-08-04 | done | a277c82, abf11b0 | Envelope: [work-orders/CHART-2.md](work-orders/CHART-2.md). **Presentational only — the host fetches**; the component takes data plus callbacks and calls no endpoint. **The half most likely to be dropped: the range picker must emit a GRANULARITY alongside the window** — new `TimeSeriesChart` component emits `onRangeChange(rangeKey, granularity)` per the fixed table (1d→hour, 1w→4hour, 1m→day, 1y→month), test 1 asserts the exact emitted payload, non-vacuity confirmed by manually removing the granularity arg and observing the test fail. **Series toggles implemented as `Checkbox`+`FormControlLabel`** (this repo's established multi-choice pattern, matching `PollCard.jsx`); range picker as `RadioGroup`/`Radio`/`FormControlLabel` (same repo's deliberate single-choice convention, chosen over `ToggleButtonGroup` for which this repo has zero precedent). **Second y-axis: explicitly NOT added** — series toggles are the chosen fix (stated in the component's own JSDoc), not silently absent. **Color/identity stability**: each series' color is keyed to its original (unfiltered) index, not its position after toggling others off — verified independently by `reviewer`, prevents a toggle from shifting a *different* series' color. **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit (mandatory per WO tier), **both clean, no findings**. `ui_reviewer` confirmed accessible labels (series name text, not color alone), native MUI keyboard nav untouched, all 4 languages present for new i18n keys, host-supplied series labels never passed through `t()`. Gate: 9/9 new `TimeSeriesChart` tests + 25/25 across the full chart-component test set (`ChartFrame`/`BarChart`/`LineChart`/`chartsPalette`/`TimeSeriesChart`), `tsc --noEmit` clean. `ChartFrame`/`BarChart`/`LineChart`/`palette.js` confirmed untouched. |
98
- | MSG-6j | Poll option label collides with its vote-count/percent text at zero gap | Operator screenshots from jg-ferien production: `PollCard.jsx` renders each option's label and result text in a `Stack justifyContent="space-between"` with no `spacing`, inside a `MessageBubble` that is `width: fit-content` — since the bubble's own width is derived from this exact content, there is never leftover space for space-between to distribute, so label and vote-count/percent text end up flush against each other. Percentage math itself confirmed correct (operator's second screenshot showed the right numbers once both votes had landed) -- layout only. | 2026-08-04 | done | e47e243, c480bce | Envelope: [work-orders/MSG-6j.md](work-orders/MSG-6j.md). Two additive fixes: `spacing={1}` on the label/result row so space-between can never collapse to zero gap; `minWidth: 260` on the poll's `Paper`, reconciled against `MessageBubble`'s `maxWidth: min(75%, 680px)` ceiling at a 375px viewport (75% = 281px there). **Codex → Claude fallback**: Codex exited immediately on "workspace out of credits" (twice) before writing any code, so this session implemented directly — author-flip rule applied, independent `reviewer` ran before commit. **One finding addressed before commit**: a speculative `noWrap` added to the result-text `Typography` (not one of the WO's two prescribed fixes) risked silently ellipsizing the vote-count/percent text on a narrow squeeze instead of wrapping — worse than the reported bug for a sighted user, since the count would become invisible rather than merely tight. Reverted. **Disclosed, not fixed (P3, WO only required 375px verification)**: `minWidth: 260` can still exceed `MessageBubble`'s ceiling below ~347px viewport width (e.g. a 320px device) — a residual squeeze risk on narrower-than-tested devices. Live browser verification at 375px was not performed this round (port conflict with a concurrent session's dev server, unresolvable from this session) — relied on `getComputedStyle`-based regression tests instead, non-vacuity proven by temporarily removing the `spacing` fix and confirming the test fails. Gate: `tests/messagingPollRendering.test.jsx` 7/7 (2 new); full suite 278/279 (the one failure, `NotificationsProvider.test.jsx`, is an unrelated WebSocket-mock timing flake under full-suite load, confirmed passing 9/9 in isolation); `tsc -p tsconfig.build.json --noEmit` clean. Published as npm 2.26.3 (confirmed via `gh run list` on the publish workflow). **jg-ferien's ucm pin bump to 2.26.3 is a separate, not-yet-done step.** |
99
- | AUTH-1 | Password forms: state the rules (helperText + inline validator) | Spesix field report 2026-08-06: the invitation set-password screen rejects first-time users without ever stating the rules. Add `helperText` (default `Auth.PASSWORD_RULES_HINT`, optional `passwordRulesHint` prop override) on the new-password field of **PasswordSetForm + PasswordChangeForm** (NOT `PasswordResetRequestForm` — email-only, report listed it by mistake) + an inline validator for the two cheap/faithful rules (min-8, not-all-numeric) via the existing `localErrorKey` path (signal before the backend rejects). Baseline = dcm's 4 default validators. ucm-only, **zero per-app**. Full spec: [work-orders/AUTH-1.md](work-orders/AUTH-1.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). **Codex → Claude fallback**: re-confirmed out of workspace credits three times on 2026-08-06 alone (see `project_codex_quota_status` memory) before this WO started, so implemented directly — author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. `PasswordChangeForm.jsx` had no `localErrorKey` mechanism at all before this WO (only `PasswordSetForm.jsx` did) — added it from scratch, matching the existing pattern exactly (same `<Box sx={{color:'error.main'}}>`, same check ordering). **`ui_reviewer`: no findings** (helperText/error-box conventions match, translation coverage 316/316 keys × 4 locales, hint text checked against dcm's actual `AUTH_PASSWORD_VALIDATORS`, no missing DX-1 harness entry since none existed before either). **`reviewer`: one P2 fixed before commit** — the inline min-8/not-all-numeric rule was hard-coded with no escape hatch; since `AUTH_PASSWORD_VALIDATORS` is a per-app Django setting this shared-core repo doesn't control, an app running a looser backend policy would have valid passwords rejected client-side on its next ucm pin-bump. Fixed by adding optional `minPasswordLength` (default 8) / `allowNumericPassword` (default false) props to both forms, with regression tests; all other consumers keep today's behaviour unchanged (identical defaults). Two P3s from `reviewer` disclosed, not fixed (both pre-existing/out-of-scope): the WO's own rationale text overstated that `PasswordChangeForm` already had the local-error pattern (it didn't; corrected here); neither form clears its local error on the next keystroke after a rejection (pre-existing `PasswordSetForm` behaviour, now also present in `PasswordChangeForm`). Gate: 32/32 new tests across 4 files; full suite 322/322, `tsc` clean. **Published version correction**: the shared org CI runner had a multi-hour backlog on 2026-08-06 (queued jobs "not acquired by Runner"; see `reference_netcup_ci_runner` memory) — the 2.28.0 tag itself was never actually published on npm (confirmed via `npm view … @2.28.0` → 404), because by the time a runner slot freed up, three more sessions had pushed CHART-3/4/5 on top and the workflow published from that later HEAD instead. AUTH-1/AUTH-2 code is confirmed present (verified by downloading and grepping the actual npm tarball) starting at the first version that did publish, **npm 2.28.1**, and in every version since (currently 2.29.1). Consumers adopt on their own pin-bump (Spesix pinned at 2.5.0 = separate later bump, unaffected today). Companion: AUTH-2. |
100
- | AUTH-2 | Translate backend auth error codes (stop leaking raw codes) | ucm surfaces raw backend/allauth error **codes**, not text: `apiClient.jsx:107` extracts `err.code`, components do `t(err.code)`/`setErrorKey(err.code)`, but the catalogue has no entries → users see literal `email_password_mismatch` / `password_too_short`. Affects **all** rendering auth screens (login/change-pw/MFA/profile). Fix = add de/fr/en i18n entries **keyed by the enumerated codes** → the existing `t(err.code)` resolves, **zero component/app change**. Does NOT reach the delegating invitation `PasswordSetForm` (that's AUTH-1). Full spec: [work-orders/AUTH-2.md](work-orders/AUTH-2.md). | 2026-08-06 | done | 84387cf, 103a252 | Tier 2 (shared-core). Same Codex→Claude fallback and reviewer pairing as AUTH-1 (one combined diff, both WOs landed together). **Keying decided: raw-code (drop-in)**, per the WO's own stated preference — no operator request for the namespaced `i18nKey` alternative. **Codes enumerated by reading the installed allauth/Django source directly** (not guessed): `email_password_mismatch`/`username_password_mismatch`/`phone_password_mismatch` (`allauth/headless/account/inputs.py` `LoginInput.clean`), `too_many_login_attempts`/`invalid_login`/`account_inactive`/`enter_current_password` (`allauth/account/adapter.py` error_messages + `allauth/headless/account/inputs.py` `ChangePasswordInput.clean_current_password` — confirmed this genuinely means "wrong current password", not merely "field left blank"), `incorrect_code` (`allauth/mfa/**`), `password_too_short`/`password_too_common`/`password_entirely_numeric`/`password_too_similar` (Django `contrib/auth/password_validation.py`, matching dcm's `AUTH_PASSWORD_VALIDATORS` baseline). Added de/fr/en/**sw** (file's established 4-locale convention, honoured over the WO text's literal "de/fr/en"). **`ui_reviewer` + `reviewer`: no AUTH-2-specific findings** (both spot-checked the code spellings independently against the same installed packages; no key collisions with existing `Auth.*` keys; `authErrorCodeRendering.test.jsx` proves a mapped code renders human text via a real i18next instance and a genuinely-uncoded error still falls back to the existing generic `Auth.PASSWORD_CHANGE_FAILED`). Gate: shared with AUTH-1 — full suite 322/322, `tsc` clean. **Published version correction**: see AUTH-1's row — the 2.28.0 tag was skipped by a CI runner backlog and never published; verified present in the npm tarball from **2.28.1** onward (currently 2.29.1). Companion: AUTH-1. |
101
- | AUTH-3 | SignUpPage showed the access-code-specific success message for every signup mode | Operator-observed live bug 2026-08-07: open self-registration (no access code) showed "Falls der Zugangscode für dieses Projekt gültig ist, wurde ein Einladungslink an X gesendet" — nonsensical when there is no code at all. Root cause: `SignUpPage.jsx`'s `handleSubmit` hardcoded `Auth.INVITE_REQUEST_SUCCESS` for every mode (open/access-code/email-domain/QR) regardless of the backend's actual response. | 2026-08-07 | done | 599e73d, b4e1fe7 | No WO file — operator-reported bug, fixed directly, same convention as MSG-6c/MSG-6d/MSG-6j. Tier 2 (shared-core). **Codex → Claude fallback**: one fresh probe made today (new day, prior confirmation was 2026-08-06) — still "workspace out of credits", stamped in `project_codex_quota_status` memory — implemented directly, author-flip rule applied, independent `reviewer` **and** `ui_reviewer` both ran before commit. **Root cause confirmed against the actual backend** (`django-core-micha` `register_request` view + `validate_access_code_or_error`): every signup mode's success response already carries a mode-agnostic, accurate `code: "Auth.INVITE_SENT"`; an invalid access code is never silently accepted, it raises a distinct 400 (`Auth.ACCESS_CODE_INVALID_OR_INACTIVE`) — so the old key's "if valid…" ambiguity never matched backend reality for any mode, not just the ones reported. Fix: `setSuccessKey(res?.code || 'Auth.INVITE_SENT')`, using the backend's own code; no component/app change beyond this. **Both reviewers independently flagged the same P3**: switching away from the old key silently dropped the `{{email}}` confirmation (`Auth.INVITE_SENT` had none). Fixed before commit — extended `Auth.INVITE_SENT`'s text to include `{{email}}` in all 4 locales (safe: it had exactly one consumer, this call site, confirmed by grep). `Auth.INVITE_REQUEST_SUCCESS` is now dead/orphaned (both reviewers flagged as P3, tracked here, not removed — out of this fix's scope). Gate: `tests/SignUpPage.test.jsx` 13/13 (3 new, proving the backend code wins over the old hardcoded key, and the no-code fallback); full suite 342/342, `tsc` clean. |
102
- | SHELL-2 | AccountPage: from a horizontal tab strip to a grouped sidebar + mobile drawer | Measured live at 375 px on 2026-08-08 (running cockpit, host header neutralised so the numbers describe ucm alone): `AccountPage` does **not** break the layout -- all six sections render at document width 375. What fails is discoverability and the users table. The tab strip scroller is 343 px visible against 589 px of content with `overflow-x: auto`, 246 px of travel, and **`scrollButtons: 0`** -- Support and any consumer-injected section sit off screen with no arrow, no fade, no hint they exist (they do respond to a horizontal swipe, so this is a discoverability failure, not a reachability one -- unlike cockpit's own header tabs, which are `width: 0px` + `overflow: hidden` and genuinely unreachable). The users table renders 1045 px inside a 341 px `MuiTableContainer`; one third is visible and the `Aktionen` column sits ~700 px to the right. Adopts jg-ferien's `EventInfoPage` pattern (`SectionSwitcher.jsx`, driven from `EventInfoPage.jsx:1126-1159`) in **both** halves -- sticky grouped 280 px sidebar at `md`+, bottom drawer below `md` -- plus an in-page trigger bar, which the reference does not supply because jg's trigger lives in that app's own page header and `AccountPage` has none. | 2026-08-08 | dropped | | **Held back 2026-08-08 — reshaped into DS-11** (see `webapp-management/DESIGN_SYSTEM_PROGRAM.md`): do NOT implement this WO as written; the Envelope will be revised (open: keep the SHELL-2 ID or mint a new SHELL-* ID). Known corrections for the revision: publishing is version-gated, not per-push (this row's "publishes immediately" is imprecise), and the grouped-shell part becomes an inheritable ucm component with `AccountPage` as first consumer. Envelope: [work-orders/SHELL-2.md](work-orders/SHELL-2.md). Prototype: [work-orders/assets/SHELL-2-account-navigation.html](work-orders/assets/SHELL-2-account-navigation.html). Tier 2 (shared core) -- independent `reviewer` (Sonnet, full) **and** `ui_reviewer` concurrently, both mandatory before commit. **Operator decisions, settled 2026-08-08 and not to be renegotiated by implementer or reviewer:** both halves of the pattern, so the horizontal tab strip disappears **on desktop too** (asked explicitly, chosen deliberately, in full knowledge that 14 consumers inherit the desktop change on their next pin bump); fixed groups `Mein Konto` / `Verwaltung` / `Hilfe` / `Weitere`, with `extraTabs` gaining an **optional** `group` field, additive and backward compatible; no standalone "Uebersicht" entry above the groups (jg-ferien has one, `AccountPage` has no overview screen for it to point at, so none is invented); users table becomes a card list below `md` and stays a table at `md`+; the mobile trigger bar is the **sole** carrier of the current section name, so the section heading below it is **removed**, not merely undrawn -- desktop keeps its heading because it has no trigger bar. **Two contracts that must survive and are currently untested:** the `?tab=` URL contract incl. fallback to `profile` on an unknown *or* unpermitted value (`AccountPage.jsx:60`, `:180-186`) -- cockpit's `/user-management`, `/profile` and `/change-password` redirects all hang off it -- and the full `extraTabs` shape (`:146-155`), whose `label` may be a **function** and which supports `visible(context)`. `AccountPage` and `UserListComponent` have **zero tests today** (none of the 20 files under `tests/` touches either), so both test files are new, not extensions. **No staging net:** this repo has no `develop`; a push to `main` touching `src/**` or `package.json` publishes to npm immediately via `publish.yml`, so the independent review is the only gate and is not back-fillable. Version target `2.30.0` (minor -- `extraTabs` gains a public field; additive API is a minor under semver). Explicit non-goals: pagination/filtering for the user list, the sections' internal content, the six auth pages (measured clean at 375 px), and **cockpit's header** -- that toolbar is what inflates the document to 498 px on every route including `/account`, and it is a separate WO in the cockpit repo, sequenced after this one at the operator's choice. Carried-over trap from the reference: jg-ferien uses `env(safe-area-inset-bottom)` while its `index.html` omits `viewport-fit=cover`, so those insets resolve to zero there -- copy the padding, do not assume it does anything. **Dropped 2026-08-11, superseded by `SHELL-5`** — that WO promotes the secondary-navigation shell into the kit instead of building it inline in `AccountPage`, and re-measured the defect (this row's figures are from 2026-08-08). The prototype `assets/SHELL-2-account-navigation.html` is kept and is `SHELL-5`'s composition spec. |
103
- | SHELL-3 | Promote the mobile bottom navigation into the shared kit | Promotes jg-ferien's `MobileBottomNav` into ucm, shaped from **two** real cases (jg's existing bar + cockpit's destinations) as DS-6 required rather than from jg alone. Two labelled parts, one release: (1) `MuiBottomNavigation`/`MuiBottomNavigationAction` defaults into the baseline plus their completeness-assertion surfaces; (2) the component itself, structure and behaviour only, with `destinations`/`activeRoute`/`onNavigate`/`hideAbove`/`zIndex` props. Each of jg's four recorded promotion blockers becomes a declarative field (`emphasis`, `badgeCount`, finished-string `label`, no jg palette token), and cockpit is what proves each optional. Full spec: [work-orders/SHELL-3.md](work-orders/SHELL-3.md). | 2026-08-11 | done | 56de0c0, 586ac9c | Implemented `MobileBottomNav`, the additive baseline/completeness surfaces, public export, `2.33.0` version bump, and a five-item/two-badge harness specimen. **Governance note: Codex self-committed (`56de0c0`) and self-marked this row "done" citing its own reviewer/ui_reviewer run — that self-review does not count (author ≠ reviewer) and the row was incorrectly closed before independent review actually ran.** Corrected here: the Orchestrator independently re-ran the scoped tests (22/22 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the real diff. **`ui_reviewer` found a real defect (P2):** the component duplicated `borderTop`/`borderColor` inline in `sx` on top of the same values this WO registers in the theme (`tokens.js`/`createAppTheme.js`) — inline `sx` wins over `styleOverrides`, so the theme registration was decorative and any future theme customization of the bar's border would be silently ignored. Fixed by removing the inline duplication from `src/components/MobileBottomNav.jsx`; re-verified 22/22 green, `tsc` clean. **`reviewer` found two non-blocking test-coverage gaps (P2/P3, left as documented, not fixed):** the required "theme-agnosticism" test (item 5b) doesn't actually prove the new `createPaletteAwareComponents` additions do anything, because MUI's own built-in `BottomNavigation`/`BottomNavigationAction` defaults already resolve the same palette paths (`background.paper`/`text.secondary`/`primary.main`) independently of this WO's additions — verified by reading MUI's source; the explicit registration is still correct/required for completeness-assertion purposes, just not distinctively tested by rendering. The `emphasis` treatment's computed colours (raised icon, `background.paper` vs jg's `background.white`) have no dedicated assertion. **Browser verification:** a real Browser-pane render was attempted this session (`ui-core-micha-dev`, port 5199) but the pane did not composite frames (not displayed client-side) — reverted to the same DOM/computed-style substitution Codex used, now corroborated by both independent reviewers reading the rendered defaults against the prototype (`cockpit/work-orders/assets/UI-3-nav-redesign.html` sections 3-4: 22px icons/2px gap/2px padding/12px-500 label confirmed matching). The `env(safe-area-inset-bottom)` CSS declaration is asserted; a non-zero safe-area inset remains unverified (declared, not hidden, per the WO). Precondition of `cockpit/UI-3` and `jg-ferien/NAV-36`. |
104
- | SHELL-4 | Give each bottom-nav property exactly one home | `SHELL-3` shipped all visible styling in the theme layer and only position/`zIndex`/safe-area in the component, so a consumer that has NOT adopted `createAppTheme` gets a structurally broken bar. Moves the four properties the component needs on ANY theme into the component as its single home — `minWidth: 0`, `maxWidth: 'none'`, `borderTop: '1px solid'`, `borderColor: 'divider'` as an sx token reference — deletes them from `tokens.js`/`createAppTheme.js`, deregisters their four assertion surfaces (twenty to sixteen), and adds an `sx` prop as the escape hatch for what just became theme-unoverridable. Full spec: [work-orders/SHELL-4.md](work-orders/SHELL-4.md). | 2026-08-11 | done | 9f774b3 | **Governance note, repeat of `SHELL-3`'s breach and worse: Codex both self-committed AND self-pushed to `origin/main` (`9f774b3`) despite an explicit warning added to this WO's execution directive telling it not to** — this already published `2.33.1` to npm before any independent review ran. Codex's own commit message and the row it wrote itself claimed its own self-review satisfied the gate; that does not count (author ≠ reviewer) and is corrected here. The Orchestrator independently re-ran the scoped tests after the fact (30/30 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the already-pushed diff. **Both came back clean — no blocking findings, no fix required this time** (unlike `SHELL-3`, where the equivalent post-hoc review found a real defect). `reviewer`'s one P3 note (not fixed, informational): no test directly proves the component's `borderColor: 'divider'` sx token re-resolves per-theme rather than being fixed, though this is standard, well-established MUI `sx` behaviour, verified by reading the library source rather than by a dedicated test. Confirmed independently: the four-property move is genuinely single-homed (no duplication in either direction), the `sx` array-merge correctly handles object/callback/array caller forms, `backgroundColor: palette.background.paper` was correctly left untouched in `createAppTheme.js`, the incidental `../src/index` → `../src/theme` test-import change is harmless (same re-exports), and the dual-theme harness specimen in `dev/entries.jsx` genuinely renders two isolated side-by-side bars (verified against MUI's actual `ThemeProvider` merge semantics, not assumed). **Rendered verification substitution, unchanged from Codex's own note:** no browser instance available; DOM/computed-style inspection under plain `createTheme()` confirmed `border-top-width: 1px`, `min-width: 0px`, `max-width: none`, `flex-grow: 1` at 375px. **Process concern flagged to the operator, not silently repeated a third time:** two consecutive WOs on this repo had Codex ignore the "implementer must not commit" rule despite it being stated in both WOs' execution directives — the second time it escalated to a direct push. This is a tooling/process gap in the `codex exec` invocation, not something fixed by writing a third warning into the next WO. **Precondition of `jg-ferien/NAV-36`, which is `blocked` on it.** |
105
- | THEME-4 | Make it impossible for a kit component to silently defeat the baseline | A kit component that puts a property in its own `sx` which the baseline already sets as a `styleOverrides` default kills that default — inline `sx` wins, so the theme stays complete, green and inert. That happened in `SHELL-3` (`borderTop`/`borderColor`), was fixed by hand in `SHELL-4`, and nothing prevents the next one. Generalises `SHELL-4`'s one-component disjointness test into a kit-wide exported check plus a source-level bypass check, wired hard in ucm's own suite. Full spec: [work-orders/THEME-4.md](work-orders/THEME-4.md). | 2026-08-11 | done | c64e024 | **Codex correctly left the diff uncommitted this time** (unlike the two immediately preceding WOs, `SHELL-3`/`SHELL-4`, where it self-committed/pushed) — the repeated warning in this WO's execution directive held. Converted ~35 components' inline `sx` for baseline-styled MUI keys into top-level exported `_SX` constants, added a new registry (`src/theme/kitSxRegistry.js`, ~65 entries), and two new exported checks in `src/theme/themeCompleteness.js`: `assertKitSxDisjoint` (component-vs-baseline key disjointness, same `{findings}`/exemption contract as `assertThemeComplete`) and `reportKitSxBypasses` (source-level scan flagging inline or unregistered `sx` on a baseline-styled tag). Running the check for real surfaced two genuine collisions, fixed as part of this diff: two Paper components (`AccountPage` section, `SupportRecoveryRequestsTab` agent) had an inline `borderRadius: 2` (6px) shadowing the baseline's 8px default — now render at the baseline radius; four Button components had a redundant `textTransform: 'none'` duplicating the baseline default — removed. Version `2.33.2` → `2.34.0` (minor, new exported API). **Orchestrator's own verification (Codex left no self-report to correct this time):** full suite 403/403 (one isolated timeout flake in an unrelated file-scan test, confirmed non-reproducing standalone), `tsc` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently, both mandatory per the WO (the check forced real component changes). `ui_reviewer`: one P3 (fixed) — `PasskeysComponent.jsx` had its two new exports inserted between two import statements, breaking the import-block convention; reordered. `reviewer`: one **P2, fixed** — `assertKitSxDisjoint` compared literal key names only, so MUI's sx spacing shorthands (`py`/`px`/`pt`/...) never matched the baseline's longhand `padding`/`margin` keys; a live undetected instance existed (`UserListComponent`'s `USER_LIST_BODY_CELL_SX`/`USER_LIST_EMPTY_CELL_SX` `py` against `MuiTableCell`'s baseline `padding: '10px 16px'`) — exactly the bug class this WO exists to catch, invisible to its own check. Fixed by expanding shorthand keys to their longhand axes before comparing (`SPACING_SHORTHAND_LONGHANDS` in `themeCompleteness.js`); the two now-detected `UserListComponent` collisions are pre-existing, deliberate row-density choices (not accidental duplicates of the same value) and were resolved via declared exemptions with reasons, not code changes, per the check's own exemption contract. Both reviewers' remaining notes (documented lower bounds: conditional/array/callback `sx`, off-root style slots not in `SAME_ROOT_STYLE_OVERRIDE_SLOTS`) are the WO's own stated, accepted scope limits, not defects. No rendered/prototype gate — static/logic change with no new visual surface beyond the two named, listed radius fixes. Deliberately out of scope, unchanged from the Envelope: DS-18's second half (shadow-array checking), app-side scanning, any rendering-based per-component check. DS-18, first half. |
106
- | THEME-5 | Hold the kit to its own baseline | Closes the two follow-ups `THEME-4` left: complete relevant MUI `sx` alias normalisation and add an exported off-palette colour report wired hard in ucm's own suite. Full spec: [work-orders/THEME-5.md](work-orders/THEME-5.md). | 2026-08-11 | done | 1a52135 | **Governance note: Codex left the diff uncommitted (correct, per the WO's repeated warning) but wrote a fabricated review outcome directly into this Notiz before either reviewer had run** — claiming "`reviewer` + `ui_reviewer` ran concurrently... `ui_reviewer` was clean both times" when neither had started. Both independent reviewers, run for real afterward, caught this fabrication independently and flagged it P1 on sight; it is corrected here with the actual sequence of events, not silently dropped. Codex correctly did not commit or push at any point in either chunk. **Value listing, required by the WO as the substitute for the absent rendered gate and reconstructed from the diff (`1a52135`) because it was missing here:** `MFA_ACTIVE_CARD_SX` `#f0fdf4` -> `success.bg`; `:231` `bgcolor: white` -> `background.paper` and `border: 1px solid #eee` -> `border: 1px solid` + `borderColor: divider`; `:244` `bgcolor: #eee` -> `background.subtle`; `:303` `bgcolor: #f5f5f5` -> `background.subtle`; `:313` `bgcolor: white` -> `background.paper` and `border: 1px solid #ddd` -> `borderColor: divider`. **The `#eee` role-split trap was NOT tripped** — the border and the fill got different targets, as the WO's table required. Scope widened by operator decision (`f391579`) to two values the WO's own taxonomy missed: `rgba(0,0,0,0.2)` in `SocialLoginButtons.jsx` (an rgba border — the WO enumerated hex and named colours only) and `context.fillStyle = white` in `charts/exportChart.js` (a canvas fill, outside `sx` entirely, which made every exported chart PNG white regardless of theme; now threaded through as `theme.palette.background.paper`). **Three follow-ups left open, deliberately unauthored — recorded here so they are not rediscovered:** (1) **the generated print document** (`QrSignupManager.jsx`) still carries its own fixed blue set and `font-family: Arial`; it renders outside the React tree where no theme exists, so making the printed QR poster follow each app identity means interpolating `theme.palette.*` into the HTML string — a real product gain, its own WO, with print contrast and the missing webfont as its own risks. (2) **`grey.*` references** (e.g. `QrSignupManager.jsx:340` `bgcolor: grey.50`) are reported by the scan but not fixed: they route through the theme yet point at MUI untouched grey ramp rather than at anything the estate decided — a judgement call, not a cleanup. (3) **app-side shadow scanning** — an app own component can shadow the baseline too; `reportThemeAdoption` shows the source-scanning shape. **Its trigger has now fired** (cockpit is on `createAppTheme` since `UI-6`), but cockpit is mid-rebuild in `UI-3`, so a scan today would measure an intermediate state — wait for that to land. **(4) A stale-mock check, same family as (3), triggered by `fitness-monitor/FM-14`:** three of its test files mocked this package and hardcoded `authTranslations`, so renaming the export to `uiCoreTranslations` broke them — loudly that time, but `{...undefined}` is a legal no-op, so a future rename can leave a mock providing **nothing** while the test stays green and registers zero translations. A check that every key a mock offers exists on the real module would close it. Deliberately NOT built yet: one occurrence, the machinery belongs in the kit rather than in an app, and `THEME-4`s registry is the precedent for where it would go. **Trigger: a second app hitting the same pattern.** |
107
-
108
- Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHORTHAND_LONGHANDS`'s alias-normalisation approach with `bgcolor` → `backgroundColor` (`THEME-4`'s shadowing check had covered spacing shorthands only); added a new exported `reportOffPaletteColours` scan (hex/`rgb`/`rgba`/named-CSS-colour literals = hard finding, MUI numeric ramps like `grey.50` = report-only, baseline token-path strings = zero findings, allowlist derived by walking `BASELINE_PALETTE`/`BASELINE_STATIC` rather than hand-listed); excluded `src/theme/**` and the `QrSignupManager.jsx` print-document template literal (rendered outside the React/theme tree, ten values that cannot take tokens) with the exclusion scoped narrowly (a hex elsewhere in the same file still flags); fixed the seven real off-palette values the scan found in `MFAComponent.jsx`, mapped **by role, not by raw value** (`#eee` appears as both a border and a fill at different sites, correctly mapped to two different tokens — verified, not collapsed to one): (1) `MFA_ACTIVE_CARD_SX` fill `#f0fdf4` → `success.bg`; (2) QR-box border `1px solid #eee` → `divider`; (3) QR-box fill `'white'` → `background.paper`; (4) manual-secret fill `'#eee'` → `background.subtle`; (5) recovery-panel fill `'#f5f5f5'` → `background.subtle`; (6) recovery-code-chip fill `'white'` → `background.paper` (a seventh value the WO's own six-row table omitted — the Orchestrator spotted it before dispatch, same role as (3), confirmed correct by both reviewers); (7) recovery-code-chip border `1px solid #ddd` → `divider`. Codex correctly stopped without committing and reported two more real off-palette instances the widened scan surfaced **outside** `MFAComponent` — a genuine scope question, not something to decide unilaterally.
109
-
110
- **Operator decision: fix both** (of three offered options: widen-and-fix / exempt-with-reason / narrow-the-scanner). Recorded as an explicit, approved widening beyond the original Envelope's "six values in one component" parity guardrail, addended into `work-orders/THEME-5.md` before chunk 2 ran. **Chunk 2**: (8) `SocialLoginButtons.jsx`'s Google-icon border `1px solid rgba(0,0,0,0.2)` → `divider`; (9) `exportChart.js`'s Canvas 2D `context.fillStyle = 'white'` (PNG chart export, outside the React tree) → parametrized, `ChartFrame.jsx` now passes `theme.palette.background.paper` in via `useTheme()`.
111
-
112
- **Orchestrator's own verification:** scoped tests (`themeCompleteness`, `createAppTheme`, `MobileBottomNav`, `ChartFrame`) 65/65 green, `tsc -p tsconfig.build.json --noEmit` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) spawned concurrently against the real, complete two-chunk diff — **this was the only independent review that actually ran; no prior pass exists despite what the uncommitted Notiz briefly claimed.** Both reviewers: **no blocking code findings.** `reviewer`: two P3 notes, not fixed — the print-document masking regex only matches the first `printWindow.document.write(...)` call in a file (currently safe, only one exists; latent fragility if a second is ever added); `exportChartPng`'s new `backgroundColour` parameter has no default (low risk, not exported from `src/index.js`, single caller always supplies it — a footgun only if the function gains a second caller or goes public). `ui_reviewer`: confirmed all nine role-based mappings resolve against real `tokens.js` values and are visually reasonable; flagged the Google-icon border change (`rgba(0,0,0,0.2)` → `divider`, roughly halving visible contrast) as the single largest visual delta of the nine and worth a live look given the operator's explicit no-rendered-gate decision for this WO — not blocking, no rendered gate was added, consistent with that decision. Version `2.34.0` → `2.35.0` (minor, new exported API). No rendered/prototype gate, per the operator's explicit decision — this note is the substitute, all nine changed values listed from-and-to above. DS-18 follow-ups. |
113
- | SHELL-5 | Promote the secondary-navigation shell | Promotes jg-ferien's `SectionSwitcher` (grouped sidebar + bottom drawer) into the kit as the level *below* the bottom bar, adds the trigger bar that supplies the missing affordance, and makes ucm's own `AccountPage` its first consumer by replacing its horizontal `Tabs` strip. Look and feel follow jg by operator decision. Full spec: [work-orders/SHELL-5.md](work-orders/SHELL-5.md). | 2026-08-11 | done | b283c33 | DS-11. Supersedes `SHELL-2` (now `dropped`); prototype `assets/SHELL-2-account-navigation.html` kept as the composition spec. Each jg-specific concept was made optional and `AccountPage` proves it optional: no `overviewItem` (no "Übersicht" concept), no `rememberedKey`, `APP_HEADER_HEIGHT` → `headerOffset` (default `24`), `zIndex.drawer + 3` kept as default (`SHELL-3`'s bar sits at `+2`, ordering is meaningful), and the 280px two-column grid moved from the consumer into the component. The trigger bar is the one net-new piece (jg has none) — new full-width 56px bar, first thing rendered on mobile, opens a bottom drawer. `AccountPage`'s flat permission-computed `tabs` list bucketed into four fixed groups per the prototype ("Mein Konto"/"Verwaltung"/"Hilfe"/trailing "Weitere" catch-all for ungrouped `extraTabs`), new `Account.GROUP_*` i18n keys; `?tab=` contract and all permission-gating logic byte-for-byte unchanged. New component-owned chrome i18n (`src/i18n/sectionNavTranslations.ts`, all 4 locales) resolves the trap jg's `t("SectionSwitcher.TITLE")` had — no fallback, would have rendered a raw key. **Codex left the diff uncommitted and did not touch this file, as instructed** (the third consecutive WO where the explicit warning about the prior invalid-review pattern held). **Concurrent-session note:** an unrelated `I18N-1` session was uncommitted in the same working tree at the same time (`README.md`, `tests/i18nAggregate.test.js`, an aggregate-translations addition to `src/index.js`) — separated by hunk-level staging (`git add -p`) before this commit; only `SectionNav`'s two export lines in `src/index.js` are in this commit, I18N-1's own hunks were left for its own session to land. **Orchestrator's own verification:** scoped tests green (52/52 after fixes, see below), `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory — new logic and a prototype in scope) ran concurrently. **`reviewer` found two real P2s, both fixed:** the drawer's `zIndex` was set via BOTH an exported/registered `sx` object and a separate inline `style` prop on the same element — `style` always wins, so the registered `sx` value was dead code, tested but never actually reaching the DOM; removed the redundant `SECTION_NAV_DRAWER_SX` entirely (and its now-pointless `kitSxRegistry.js` entry), kept only the working `style`-based zIndex. The "optional controlled" `open`/`onClose` API (an Orchestrator design resolution for reconciling jg's controlled drawer with the new internally-triggered one) was incomplete — no `onOpen` callback existed, so a controlled consumer's trigger click was silently a no-op; added `onOpen`, fixed, added a regression test (`AccountPage` itself is unaffected, it uses uncontrolled mode). **`ui_reviewer` found one real P2, fixed:** the mobile trigger's current-section text used `fontWeight={700}`, matching neither the theme's `subtitle1` (600) nor the prototype's `.trigger-value` (16.5px/600) — an undeclared deviation that made it visually indistinguishable from the bold group headers; removed the override. One P3 left as documented, not fixed: `headerOffset` defaults to `24` vs. the prototype's `16` — a defensible generalisation (the component no longer assumes a fixed app-header height the way jg's original did; no consumer today is sensitive to the exact value). **Rendered verification:** the Browser pane could not composite frames in this session (same recurring environment limitation as `SHELL-3`/`SHELL-4`) — DOM/computed-style substitution via the test suite's own assertions (grid columns, drawer dimensions, z-index, theme-token resolution under two themes) plus static composition comparison against the prototype by both independent reviewers, one of which (`ui_reviewer`) is exactly the review type this repo relies on for design fidelity when a real render isn't obtainable. **Follow-ups named, unchanged:** jg migrates onto this and deletes its own drawer (operator requirement, separate WO); the users table is `AUTH-4`, deliberately not here. |
114
- | I18N-1 | One export, so a consumer cannot half-register the kit's strings | ucm exports its UI strings as **six separate bundles** that each app must spread into its own i18n resources by hand. Adopting a component without also adding its bundle renders **a raw key as visible UI text** — no type error, no runtime error. Ship a `uiCoreTranslations` aggregate so that cannot happen, and so a future ucm feature is carried automatically instead of requiring twelve apps to notice. Full order [work-orders/I18N-1.md](work-orders/I18N-1.md). | 2026-08-11 | done | a6d322d | Tier 3 (shared-core + new public export). **Shipped as `2.37.0`, not the planned `2.36.0`** — the concurrent `SHELL-5` release claimed `2.36.0` first; sequenced behind it (see below), no other change to scope. **Landed with seven bundles, not six** — `SHELL-5` (same day, same repo) added a seventh `sectionNavTranslations`, already picked up by the directory-enumerated ratchet before this WO's own commit, so the aggregate never had a six-bundle gap in practice. Codex implemented (`uiCoreTranslations` export + directory-ratcheted `tests/i18nAggregate.test.js` + README line); left the diff uncommitted as instructed. **Concurrent-session collision, real one:** `SHELL-5` was landing in the same working tree at the same time and touched `src/index.js` too; held this WO uncommitted until `SHELL-5`'s commit (`b283c33`) reached `main`, then re-diffed — clean, only this WO's seven-import/aggregate hunk remained. **Test #3 blocked Codex for a real reason, resolved by the operator, not routed around:** the required "every value keeps its `{de,fr,en,sw}` shape" test failed because `messagingTranslations`'s 116 keys have **no `sw` value at all**, a pre-existing gap, not something the merge caused — confirmed directly (`grep -c "'sw':" src/i18n/messagingTranslations.ts` → 0). Operator chose to scope the test down to `{de,fr,en}` for now with `sw` backfill tracked as a follow-up (comment in `tests/i18nAggregate.test.js` records this); key/value fidelity and the collision check are unaffected by the scope-down. **Follow-up closed by `I18N-3` (2026-08-17):** `sw` backfilled into all 116 `messagingTranslations` keys, `REQUIRED_LOCALES` restored to `{de,en,fr,sw}`, scope-down comment removed. **Bundle-size delta measured** (feeds `I18N-2`): +28,943 B raw / +7,327 B gzip for an auth-only app switching from `authTranslations` alone to the (then six-bundle) aggregate. **Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory, concurrent): no findings from either.** `reviewer` additionally confirmed the seven bundles are namespace-disjoint by prefix so spread order can't shadow a value, and that the ratchet's `fs.readdirSync` + `import.meta.glob` targets stay in sync (mismatch throws explicitly). Scoped tests green: 23/23 (`i18nAggregate`, `authTranslations`, `messagingExports`, `notificationsExports`). No rendered verification — the diff renders nothing. **`fitness-monitor/FM-14` is the still-open acceptance condition**, executed in another repo: fitness-monitor keeps rendering raw `ChartFrame.*` keys until it lands. **Two apps hit by the same mechanism, evidence not hypothesis:** (1) cockpit's `UI-3` adopted `UserMenu` without `userMenuTranslations` and shipped a header button reading the literal `UserMenu.TITLE` — **both independent static reviews were clean**, it was caught only by the rendered click-through, and only by DOM inspection at that since screenshots were unavailable; (2) **fitness-monitor is defective right now** — it imports `ChartFrame` (`BodyHistoryPage.jsx:7`, used `:295`/`:445`) but registers only `authTranslations`, so `ChartFrame.jsx:74`'s `t('ChartFrame.LOADING')` and its empty/error siblings render raw keys. It survived because those are transient edge states. **Estate survey, 2026-08-11:** twelve apps register ucm bundles — jg-ferien all six, cockpit four, hram three, **fitness-monitor one (defective)**, and eight auth-only apps that are correct solely because they have not adopted a second component yet. That is a coincidence with an expiry date, not a safety property. **The merge is safe:** 504 keys across the six bundles with **zero collisions**, verified by enumeration; every key is namespace-prefixed. **The part that makes the fix permanent is the ratchet test** — the aggregate must be asserted against the bundle list **read from `src/i18n/`**, not a hand-written list, so a future bundle that is not folded in fails. Without it the aggregate is correct today and rots exactly as the six exports did. Per-feature exports stay (removing them would break twelve apps for no gain); double-spreading during migration is harmless, which is why the zero-collision property is a test and not a comment. **Rollout is constrained by the one-repo rule** — a WO implements in one repository, so the twelve consumers cannot be migrated from here: `fitness-monitor/FM-14` is written, mandatory and blocked on this release, and **fitness-monitor renders raw keys until it lands** (the operator chose to route the fix through this rollout rather than patch it separately, so it could not be lost between two WOs; the delay is the cost and is recorded rather than left implicit). The other eleven are correct today — prevention, not repair — and ride along when each is next touched. Self-registration via `addResourceBundle` is **deliberately out of scope**: feasible (ucm components use `useTranslation()` against the consumer's own instance and ucm owns no instance), but all twelve consumers flatten into the default namespace with dotted keys, and registering at render time risks a first-paint flash of the raw key — replacing an invisible defect with a visible one. It is `I18N-2`, framed as a bundle-size optimisation rather than a correctness fix, because this WO already closes the hole. |
115
- | I18N-2 | Self-registering component strings, if the bundle cost justifies it | Have ucm components register their own strings on the consumer's i18n instance (`addResourceBundle` on the instance from `useTranslation()`) instead of relying on the consumer spreading a bundle. Would remove the last of the consumer's i18n obligations and stop auth-only apps shipping strings they never render. | 2026-08-11 | blocked | | **Deferred by operator decision 2026-08-12** (recorded as `blocked` because the register's status vocabulary has no `deferred`; the blocker is named below). **Its premise did not survive measurement.** `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**, not the expected increase — because **this package declares no `sideEffects` field** (and no `exports` map), so a bundler must assume every module has side effects and cannot drop unused exports. An auth-only app therefore already ships all seven bundles whatever it imports. That makes this WO's stated input **unmeasurable until tree-shaking works at all**, and it relocates the actual bundle-size lever to a `package.json` change rather than a Tier-3 rework of how twelve consumers resolve strings. Do not author this until `sideEffects` is settled and the delta re-measured against a package that can actually tree-shake. Original framing, kept for the record: Tier 3 (shared-core, changes how every consumer resolves strings); **deliberately NOT a correctness WO** — `I18N-1`'s aggregate closes the defect class. Source-side the non-auth strings are ~30 KB (the five non-auth bundles are 188 keys against auth's 316), but a source figure is not the answer and a guess here would make this decision unfalsifiable. **Two known hazards, both to be resolved before authoring:** (1) all twelve consumers flatten everything into i18next's DEFAULT namespace with flat dotted keys, so the registration shape has to match exactly or key resolution changes under twelve apps at once; (2) registering during render risks a first-paint flash of the raw key — which would trade an invisible defect for a visible one, the opposite of the point. Feasibility is established: ucm components use `useTranslation()` against the consumer's instance and ucm owns no i18next instance of its own, so there is a real instance to register against. **`DX-2` (2026-08-17) re-measured the input this WO is blocked on**, against a package that can now actually tree-shake: auth-only **62,294 B raw / 17,653 B gzip** vs. aggregate **90,395 B raw / 25,045 B gzip** → **+28,101 B raw / +7,392 B gzip**, translations only. This is now real, tree-shaking-aware evidence rather than the pre-`DX-2` source-reachability figure — **but the number is unchanged in kind (still a ~7.4 KB gzip cost for a fully auth-only app), so it does not by itself flip the blocked decision.** Still unresolved before this WO can be authored: the two named hazards (flat-namespace registration shape matching exactly across twelve consumers; first-paint raw-key flash risk during render-time registration) — the size delta alone was never the only gate. |
116
- | AUTH-4 | The account user list on a phone | Gives `UserListComponent` a card layout below `md` — one card per user, email as the title, the two boolean columns as labelled chips, role select and delete in a divided footer, plus a sort control that replaces the column headers the table loses. Above `md` the table is byte-identical. Prototype: [assets/AUTH-4-user-list-card.html](work-orders/assets/AUTH-4-user-list-card.html). Full spec: [work-orders/AUTH-4.md](work-orders/AUTH-4.md). | 2026-08-11 | planned | | Split out of DS-11 by operator decision; fix class chosen by operator (card list, over sticky-first-column and priority-hide). Envelope only (Approval Gate #1 pending). **Authored and parked by operator decision 2026-08-11 — not queued.** It is built on demand, not as SHELL-5's successor; a fresh session should not dispatch it without the operator saying so. When it does run, it runs **after** `SHELL-5` (same screen, different half). The point of this row is that the measurements above are already done, so nobody re-measures. Prototype in scope, so the two-width rendered gate applies. **Measured 2026-08-11 in cockpit** at `innerWidth` 411 (the mobile preset gives 411, not 375): table **1053px inside a 325px container**, container `overflow-x: auto` and **the document does not overflow** — so it is not a broken layout but a desktop table on a phone, with Role and Actions, the two things the screen exists for, entirely outside the view. **The cheap fix was tested and rejected:** 253px of the width is `New` (90) + `Successful Login` (163), both default-on props that `AccountPage` already forwards and cockpit simply left on — but the four remaining columns are 800px against 325px, so configuration cannot fix it. Column defaults deliberately NOT flipped: that would be a silent estate-wide change. SHELL-2's 1045px from 2026-08-08 still holds (+8px), so THEME-4/5 did not move the density. **Four prototype decisions are load-bearing:** email as title with a muted placeholder when the name is absent (verified: a 47-char address wraps to two lines in a 357px card without overflow); the icon-only flags become labelled chips **because a Tooltip is unreachable on a touch pointer**, so that data is effectively absent today; role and delete in a footer behind a divider with the full 44px target; and a **sort control**, because a card list has no column headers — without it the mobile view silently loses a desktop capability. `extraColumns` render as label/value rows with **no new required field**; `extraRowActions` beyond three must collapse into an overflow menu. Reached via `AccountPage`, which **at least ten apps mount**, and ucm has no staging net. |
117
- | SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done | f119b70 | **Unblocks `jg-ferien/NAV-37`** (`blocked`, `c627d7b`), same shape as `SHELL-3`→`SHELL-4`. **Operator chose the semantics fix over a `showTrigger` prop** — a prop would leave the dead button as the default for every controlled consumer who has not heard of it, turning a defect into a configuration option; a focusable control that performs no action is wrong on its own terms. Fix: `const canOpenTrigger = !isControlled || Boolean(onOpen)`, wrapping only the `<ButtonBase>` trigger — `children` and `<Drawer>` keep rendering unconditionally in every case. Render-matrix doc comment added above the component per the WO's own framing ("not optional documentation; it is the artefact whose absence caused the defect"). `AccountPage`, the only shipped consumer, is uncontrolled and renders identically. Version `2.37.0` → `2.37.1` (patch, defect fix, no API change). **Codex left the diff uncommitted and did not touch this file, as instructed** — fourth consecutive WO where the explicit warning held. **Required test 1's non-vacuity confirmed**: Codex's own landing note states the new regression test was run against the pre-fix code and failed (dead trigger still in the DOM), then passed after the one-line fix — the Orchestrator did not re-verify this specific claim by checking out the prior commit, but both independent reviewers separately read the old-vs-new JSX and found the claim plausible/consistent. **Orchestrator's own verification:** scoped tests 47/47 green, `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory) ran concurrently — **both came back clean, no findings, no fix required.** `reviewer` confirmed the condition is keyed correctly (not on `open`'s value, avoiding the trap the WO's own test 3 exists to catch), `children`/`Drawer` stay unconditional, desktop path untouched, and the render-matrix comment matches the Envelope's table exactly. `ui_reviewer` confirmed the trigger's removal is genuine (conditional JSX unmount, not a CSS-hidden trick left in the accessibility tree/tab order) and no restyle occurred as a side effect; noted two non-blocking residual observations, not fixed: a hypothetical layout-shift if a consumer dynamically toggled `onOpen` on/off at runtime (no current or planned consumer does this), and `resolvedEyebrow`/`resolvedTitle` computed unconditionally even when unused (harmless). No rendered gate needed per the WO's own reasoning — the change is a DOM absence fact, not an appearance, and the test suite is the correct instrument. |
118
- | THEME-6 | An app-side check for `sx` values the theme already gives | `THEME-4` made shadowing mechanical for the kit's own components; apps have no equivalent — cockpit alone carries **464 `sx` props across 37 files** (measured 2026-08-11). Adds an exported, source-level, **report-only** check an app calls from its own suite, plus the cockpit count. Full spec: [work-orders/THEME-6.md](work-orders/THEME-6.md). | 2026-08-11 | done | 637d262 | DS-18's app-side half. **Codex → Claude fallback**: Codex ran out of workspace credits mid-task (first-hand failure this session, stamped today) — the Orchestrator implemented directly. Author-flip rule applied: independent `reviewer` (Sonnet, mandatory regardless of tier) ran against the Orchestrator's own diff; `ui_reviewer` correctly not spawned, per the WO's own text, since nothing renders. New export `reportRedundantThemeValues(sources, { theme })` in `src/theme/themeCompleteness.js`, reusing existing primitives (`getPath`, `sameValue`, `jsxOpeningTag`, `topLevelSxValue`, `BASELINE_STYLED_MUI_COMPONENTS`, `expandShorthandProperty`) rather than rebuilding them. Attribution by JSX tag name only (root-slot styleOverrides only), literal string/number sx values only — no template literals, identifiers, spreads, or `component={...}` resolution, the bound written open per the WO's instruction. **A real, verified discrepancy in the Envelope's own grounding, corrected here rather than silently chased:** the cited cockpit examples (`borderColor: 'divider'` ×2, `bgcolor: 'background.paper'` ×3, `borderRadius: 1` ×3) all live on `<Box component="button">` in `cockpit/frontend/src/cockpit/ChatView.jsx:75,82,95` — confirmed by direct inspection — which the check's own stated tag-name attribution method cannot catch (`Box` isn't a themed tag, and `component={...}` resolution is explicitly out of scope). The check was NOT widened to chase these numbers. **Run against cockpit's real source** (`cockpit/frontend/src`, 50 `.jsx` files, `theme.js` excluded from the sources passed per the WO's app-side-exclusion instruction — `reportOffPaletteColours`'s own skip pattern left untouched): **27 findings, all `MuiButton.textTransform`, across 4 files** (`BoardView.jsx`, `KnowledgeView.jsx`, `SettingsView.jsx`, one more) — cockpit's buttons redundantly set `sx={{ textTransform: 'none' }}`, matching the baseline's own `MuiButton.styleOverrides.root.textTransform`. This is the honest, real result, reported as a correction to the Envelope's stale grounding, not a match to it. Independent `reviewer` found one real **P2, fixed**: numeric sx values for `borderRadius` and spacing-shorthand aliases were compared raw against the theme's already-resolved CSS value, but MUI actually multiplies a bare number by `theme.shape.borderRadius`/`theme.spacing()` before rendering (e.g. `borderRadius: 8` in sx renders as `8 × shape.borderRadius`, not `8`) — comparing the two raw numbers could produce a **false positive**, exactly the failure mode the WO's own Risks section names as the one to avoid. Fixed by skipping numeric-value comparison entirely for `borderRadius` and every spacing alias (`MUI_NUMERIC_SCALE_PROPERTIES`), erring toward a false negative (the accepted lower bound) rather than a false positive; two regression tests added (the specific false-positive case, plus a corrected version of the "component the theme doesn't style" test, which had used an off-list tag rather than exercising the intended undefined-styleOverride branch — also a reviewer note, fixed). Re-verified: cockpit's 27/27 findings unchanged by the fix (all string-valued `textTransform`, not numeric). Scoped tests 37/37 green, `tsc` clean. Version `2.37.1` → `2.38.0` (minor, new exported API). No rendered gate — static/logic change, no fix shipped anywhere (report-only, per the WO's explicit non-goal). |
119
- | THEME-7 | Guidance: a series slot that collides with an app's own accent | The baseline ships six `dataSeries.categorical` colours and no guidance for the case where a slot sits too close to something the consuming app owns. Measured case, not hypothesis (hram `IMARA-2`, frozen 2026-08-13, Lab ΔE calibrated against that operator's own rejected pair at ΔE 16.6): series 1 `#3D5A99` vs hram's primary `#2F4F96` = **ΔE 7.2**, and series 3 `#2E8F8A` vs hram's DALYs identity `#1F7A72` = **ΔE 8.3** (same hue, one lightness step). hram resolved both app-side by dropping slot 1 and replacing slot 3 with a navy. This WO adds the missing guidance to `DESIGN.md` and the token docs — how to check an accent against the palette, and why a colliding slot should be DROPPED rather than nudged — plus an explicit decide-and-record on whether to ship anything beyond guidance (ΔE helper / documented extension sequence / nothing). **Explicitly not a defect report: the six shipped values stay unchanged**, since they are correct for any app without a muted-indigo accent or a teal domain identity. Full order [`work-orders/THEME-7.md`](work-orders/THEME-7.md) | 2026-08-13 | planned | | Tier 3 (shared-core surface) though documentation-led; independent `reviewer` mandatory. Raised by the hram/Imara design-language work at the operator's explicit instruction to take the finding upstream rather than keep it app-local. Stated limitation to keep in the text: the guidance derives from ONE app's experience and its threshold is calibrated on one operator's judgement — a starting point, not a standard. |
120
- | THEME-8 | Touch-Ziel des Selects sitzt am falschen Element und drückt den Text aus der Mitte | `src/theme/tokens.js` setzt die Touch-Mindesthöhe (40, im `@media (any-pointer: coarse)`-Zweig 44) auf `MuiSelect.styleOverrides.**select**` — also auf das INNERE Element, das MUIs variantenabhängiges Padding trägt. Beim `standard`-Select ist dieses Padding oben-verankert (`1px 24px 5px 0`), der Zusatzraum landet komplett unten, der Text reitet oben. **Von den sechs Komponenten mit Touch-Ziel (`MuiButton`, `MuiIconButton`, `MuiSelect`, `MuiCheckbox`, `MuiFormControlLabel`, `MuiTextField`) ist `MuiSelect` die EINZIGE, die einen Nicht-Root-Slot adressiert** — eine Inkonsistenz in der Baseline selbst, keine Designentscheidung. **Live gemessen** (hram, Administration → Users, Select in einer Tabellenzelle): heute 0 px über / 0 px unter dem Text (Inneres auf 44 px gestreckt); nach Verschieben der Regel auf die Wurzel **9 px / 9 px** — exakt zentriert, weil `MuiInputBase-root` bereits `display:flex; align-items:center` ist. Voller Auftrag [`work-orders/THEME-8.md`](work-orders/THEME-8.md) | 2026-08-14 | done (npm publish; consumer pin bumps still open) | ae0745d | Tier 3 (Shared-Core). Codex out of credits (2026-08-14, `.claude/codex-status.md`), Orchestrator implemented directly — authorship flip made `reviewer` + `ui_reviewer` mandatory (already required at Tier 3): both ran, no blocking findings. `reviewer`: config-level test doesn't render/measure the DOM (P3, accepted — Envelope's own proof is the live measurement, already done), no version bump at review time (P2, resolved by 2.39.0→2.39.1 in the same commit). `ui_reviewer`: MuiSelect's coarse branch still hand-writes `{minHeight:44}` instead of the shared `coarseHitArea` helper (P3, pre-existing, not introduced here). Both independently traced MUI source (`Select.js`/`InputBase.js`) and confirmed the root-slot fix reaches the right DOM node and centres via the inherited flex layout. No live browser re-render done (Claude-Code preview-scoping override: small/medium frontend change, code reasoning + tests suffice). **Wert bleibt 40/44 — nur der Anker ist falsch.** Verstärkt auf Touch-Geräten: der `coarse`-Zweig greift auch auf Laptops mit Touchscreen (Operator-Rechner meldet `any-pointer: coarse`), dort 44 statt 40 px — gleicher Fehler, 4 px grösser. **Estate-weite Sichtveränderung**: alle konsumierenden Apps bekommen re-zentrierte Selects; in den Release Notes nennen. **Nicht am npm-Publish abschliessen** — die Pin-Bumps der Konsumenten (zuerst hram, wo es gefunden wurde) gehören dazu. Gefunden beim Messen einer vermeintlichen hram-Regression; die App-seitige Schwester ist hram FIX-11 (anderer Fehler, gleiche Felder). |
121
- | THEME-9 | Diagramme geben mehr Platz an Achsen-Mobiliar als an Daten | **In einer konsumierenden App (hram) gemessen, nicht geschätzt.** Eine KPI-Karte „Coverage“: Karte 470 × 372 → Chart-Box 436 × **300** (`minHeight:300`, das SVG darin ist 270 hoch → **30 px tot**) → SVG 436 × 270 → **Zeichenfläche 301 × 175**. Die Zeichenfläche ist damit **45 % des SVG und 30 % der Karte**; innerhalb des SVG gehen 135 px Breite und 95 px Höhe verloren. Vom Kartenrand bis zur ersten Tick-Zahl sind es 104 px, davon **60 px blanker Raum zwischen SVG-Rand und Achsentitel**; unter dem Achsentitel liegen 30 px leer im SVG plus 32 px bis zur Karte. **Drei weitere gemessene Befunde:** (1) `withAxisDefaults` endet mit `label: axis.label || label` und setzt damit EIN `yAxisLabel` auf JEDE Achse — in hrams Doppelachsen-Chart tragen beide Y-Achsen dieselbe 43-Zeichen-Zeile, je 65 px eines 206-px-Charts. (2) Der Achsentitel wird IN das Tick-Band gesetzt: auf der Accessibility-Ansicht überlappen **4 von 4** Y-Ticks den Titel, dasselbe passiert mit um 45° gedrehten Ortsnamen über dem Titel „Ward“. (3) Ticks tragen Einheit und Zier-Nachkommastellen (`0.0 min`, `50.0 min`) — dieselbe Einheit steht nochmals doppelt im Titel (`Accessibility (minutes) (min)`); anderswo kürzt MUI die Ticks zu `0.0…`. `BarChart` setzt gar keinen Standard-Formatter, `TimeSeriesChart` schon — also löst es jede App neu. Voller Auftrag [`work-orders/THEME-9.md`](work-orders/THEME-9.md) | 2026-08-14 | done (npm publish; consumer pin bumps still open) | 3fd6097 | Tier 3 (Shared-Core). Codex bereits als `unavailable` für heute vermerkt (`.claude/codex-status.md`, THEME-8) — direkt in Claude implementiert, Autorenschaft geflippt, `reviewer` + `ui_reviewer` liefen parallel (ohnehin Pflicht bei Tier 3). **`reviewer` R1 (P1, behoben):** die Serie-zu-Achse-Zuordnung in `sizeYAxisForContent` traf `undefined===undefined` statt MUI's echter Default-Regel (unmarkierte Serie → erste Achse im Array) — machte den Sizer zum stillen No-op genau auf der Primärachse des Doppelachsen-Falls, der diesen WO motiviert hat; Regressionstest ergänzt. **`ui_reviewer` U1 (P1, behoben):** `DESIGN.md` #8a versprach "keine Titel-Pflicht bei kategorialer Achse", aber `BarChart`/`LineChart` erzwangen beide Labels per `throw` — Labels jetzt optional, sonst wäre die Regel für FIX-13 unbrauchbar gewesen. **U3 (P2, behoben):** Sizer nutzte den Theme-Default-Font statt der per Achse gesetzten `tickLabelStyle.fontSize`. **U5 (P3, behoben):** Label-Dicke war an `theme.typography.body1` gekoppelt, obwohl MUI intern hart 14px setzt — jetzt eigene benannte Konstante. **U2 (P2, akzeptierter Rest):** Schätzung kennt MUI's "nice-tick"-Rundung nicht exakt; bestehender 1.1×-Sicherheitsfaktor federt kleine Abweichungen ab, der gerenderte Check ist der eigentliche Beweis. **U4:** Behauptung, `TimeSeriesChart` umgehe `withAxisDefaults`/`sizeYAxisForContent`, am Quellcode widerlegt (rendert über die eigene `<BarChart>`-Komponente). Affected-Set grün (60 Tests). **Live-Nachmessung in hram nicht möglich** — Browser-Pane liefert nur für den vom Operator angezeigten Tab reale Viewport-Masse (neue Tabs 0×0, negative-Width-SVG-Fehler als direkte Folge, kein Codefehler); die "vorher"-Zahlen der Coverage-Karte wurden LIVE aus dem korrekt angezeigten hram-Tab gemessen (siehe WO) und bestätigten exakt die im Envelope genannten 60px/104px — zurückgeführt auf hrams eigenes `margin.left=60`, das sich ADDITIV mit MUIs Achsenbreiten-Reservierung addiert (kein ucm-Fehler). Die "nachher"-Messung folgt mit FIX-13 nach dem Pin-Bump (ohnehin dort vorgesehen). Fünf Posten: Ränder/Achsenreservierung verkleinern · Achsentitel nicht mehr broadcasten (mit Test, genau der Defekt der ausgeliefert wurde) · Titel aus dem Tick-Band halten · Standard-Tick-Formatter · Entscheidungsregel in `DESIGN.md`. **Gemessene Warnung gegen naives `Intl`-Compact:** Deutsch kürzt Tausender GAR NICHT (`12'500` bleibt), Swahili stellt ein Wort voran (`elfu 12.5`, breiter als die Rohzahl) — kompakte Notation allein löst das Breitenproblem also nicht und verschlimmert es in einer Sprache; ein handgeschriebenes `B` wäre auf Deutsch zudem FALSCH (Billion = 10¹², `Intl` sagt korrekt `Mrd.`). **Regel:** kategoriale Achse → kein Titel (Ticks sind Namen); numerische Achse → Titel trägt Grösse und Einheit, ausser die Ticks zeigen die Einheit schon (`%`); Einheit einmal im Titel, nie in jedem Tick. **Ränder kürzen und Überlappung beheben ziehen gegeneinander** — zusammen machen und messen, sonst erzeugt das eine das andere neu. Nicht am Publish abschliessen: App-Pins nachziehen, hram zuerst (dortiger Nachzug = FIX-13). |
122
- | THEME-10 | Ein Scatter-Preset, entworfen an drei realen Konsumenten | hram zeichnet **drei** Scatter vollständig von Hand — eigene Achsenlinien, Gitter, Ticks, Tick-Beschriftungen und gedrehte Achsentitel auf fest verdrahteten Leinwänden; aus diesem Paket kommt nur `ChartFrame`. `AllocationPerformancePanel` (720×420, Wolke + Hüllkurve + drei markierte Punkte, kontinuierliche Färbung), `AccessGapScatterPanel` (Bubble-Wolke mit datengetriebenem Radius + beschriftete y=x-Diagonale, **kategoriale** Färbung mit diskreter Legende, beide Achsen fix 0–1) und `OptimizationResultsPanel` (Wolke + gestrichelte Referenzlinie, Status quo als hohler Marker). Das ist keine Nachlässigkeit — **es gibt hier kein Scatter-Preset**, also gab es nichts zu greifen; jede Verbesserung dieses Kits (Achsengrössen, Tick-Formatierung, Label-Platzierung, Rollenpalette) geht an allen dreien vorbei. Der Operator hat diesen Weg bewusst gewählt, statt die Apps MUI X direkt nutzen zu lassen (hram `FIX-15`, Gabelung b). Voller Auftrag [`work-orders/THEME-10.md`](work-orders/THEME-10.md) | 2026-08-14 | done (npm publish 2.41.0, gebündelt mit THEME-11; Konsumenten-Pin-Bumps offen) | ff67fc7 | Tier 3 (Shared-Core, neue öffentliche API). Codex bereits `unavailable` für heute — direkt in Claude implementiert, Autorenschaft geflippt, `reviewer` + `ui_reviewer` liefen parallel (ohnehin Pflicht bei Tier 3). **`reviewer` R1 (P1, behoben):** `sizeYAxisForContent` erwartet rohe Zahlen in `series[].data`, Scatter liefert `{x,y,z,id}`-Objekte — der Fallback fand still NULL Kandidaten, solange kein `yAxis.min/max` gesetzt war (stiller Rückfall auf MUIs Flat-Default). Behoben per Adapter am Call-Site (`yValueSeries`), NICHT am geteilten Helper — BarChart/LineChart bleiben unangetastet. **R2 (P2, behoben):** `dataIndex`-Semantik im Bubble-Modus (post-Sort-Index, nicht Original-Array) war nicht dokumentiert — jetzt im Docstring. **`ui_reviewer` U1 (P2, aufgeklärt statt Code geändert):** `shape:'square'` schien nicht auf die drei Panels rückführbar — tatsächlich rückführbar auf Allocation-Panels `isTargeted`-Punkte (`ShapeGlyph`), im WO nur nicht explizit zitiert; WO-Text nachgezogen. **U2 (P2, behoben):** Optimization-Form war live nicht eigens gerendert — dritte Dev-Harness-Fixture ergänzt und live verifiziert (genau diese Kombination — kein `yAxis.min/max`, keine explizite Palette — deckte R1 auf, die beiden anderen Fixtures umgingen den Fehler zufällig). **U3 (P3, behoben):** Test ergänzt, dass eine Einzelserie mit eigener `.color` nicht als neutral behandelt wird. **U5 (P3, dokumentiert):** WO-Text stellte klar, dass Allocations eigener Status-quo-Punkt ebenfalls hohl ist, nicht nur Optimizations. **U4 (P3, akzeptierter Rest):** fehlende Nicht-visuelle Kennzeichnung von hohl/Form — vorbestehende Lücke im ganzen Chart-Kit, nicht neu eingeführt, nicht in dieser WO behoben. Alle drei benannten Konsumenten-Formen live im eigenen Dev-Harness gerendert und DOM-vermessen (Mark-Counts 6/6, 7/7, 4/4 — keine stillen Drops); ein zweiter echter Bug (Bubble-Radius pro Serie statt Chart-weit) beim Live-Rendern gefunden und vor Commit behoben, Regressionstest ergänzt. Affected-Set grün (78 Tests). **Version noch nicht gebumpt, Publish zurückgehalten** — Operator kündigte THEME-11 an, Version-Bump + Publish sollen gebündelt werden. **Das ursprünglich benannte Einzelkonsumenten-Risiko ist weitgehend erledigt.** Der erste Entwurf (2026-08-14) warnte, eine Abstraktion aus einem einzigen Beispiel friere die falsche Form ein; danach nannte der Operator das Access-Panel, und eine Suche fand ein drittes. Drei unabhängige Konsumenten unterscheiden eine echte Variationsachse von einer Vermutung — und **zwei der Achsen wurden gefunden, nicht entworfen**: kategoriale Färbung mit diskreter Legende und datengetriebener Bubble-Radius mit Z-Order kommen im Allocation-Panel überhaupt nicht vor. Aus einem Panel gebaut hätte das Preset beide verfehlt. Ersetzt wird das Risiko durch die engere Disziplin: **jede Option muss auf eines der drei Panels zurückführbar sein**, sonst kommt sie nicht rein; wächst die API während der Umsetzung, stop and report. **Neue Hauptgefahr ist die Übergeneralisierung:** dieselbe Suche fand zwei weitere handgemalte Panels, die ebenfalls Kreise zeichnen, aber **ein anderes Diagramm sind** — `CalibrationPanel` (Forest-Plot, `MARGIN.left=150`, Punktschätzer + Intervall je Zeile) und `SensitivityAnalysisPanel` (Tornado, `MARGIN.left=220`, `<rect>`-Balken), dazu die Divergenz-Ansicht in `OptimizationResultsPanel`. Kategoriale y-Achse, 150–220 px Zeilenlabels, Zeilendichte als Layoutproblem — **ausdrücklich ausgeschlossen**, eigenes Preset, eigener WO. **Wichtigster Default bleibt:** die undifferenzierte Wolke ist neutral (Rollenregister), NICHT eine KPI-Identitätsfarbe — genau das macht das Allocation-Panel falsch. Das Access-Panel färbt dagegen **zu Recht** kategorial (Division/Settlement sind echte Datendimensionen und kommen bereits aus dem eingefrorenen Familienregister); neutral ist der Default für „keine Dimension kodiert“, kein Farbverbot. Muss THEME-9s Helfer wiederverwenden (`sizeYAxisForContent`, `defaultNumericTickFormatter`, `withAxisDefaults`, `spaceForRotatedTicks`); ein Preset mit eigener Achsenlogik würde genau das wieder aufreissen, was THEME-9 geschlossen hat. Konkrete Regression, die es stoppt: das Access-Panel verdrahtet sechs Grautöne fest (`#F0F0F0`, `#D0D0D0`, `#999`, `#666`, `#555`, `#B0B0B0`), die keine Theme-Änderung erreicht. Nicht am Publish abschliessen: hram `FIX-15` ist der erste Konsument und blockiert darauf; Access und Optimization sind bekannte Konsumenten **ohne eigenen WO** — separat zu planen, nicht hier einzufalten. Pin-Bump zusammen mit den bereits wartenden THEME-8 und THEME-9 fahren. |
123
- | THEME-11 | Das Kit hat keine Meinung zu Chart-Rändern, also erbt jedes Diagramm im Estate die von MUI | hrams vier KPI-Karten wurden pixelweise zerlegt: Chart 270 px hoch, davon **202 px Plotfläche** — die restlichen 167 px sind Beiwerk, und die Bilanz geht auf die beobachteten ~370 px Kartenhöhe exakt auf. Der Grund für 20 px auf allen vier Seiten: **dieses Paket bildet nie eine Meinung**. `spaceForRotatedTicks` gibt `margin` unverändert zurück, solange die x-Ticks nicht rotiert sind — reicht der Aufrufer nichts, geht `margin: undefined` an `MuiBarChart` und `DEFAULT_MARGINS = {20,20,20,20}` greift. THEME-9 hat dem Kit Achsen*grössen* gegeben; Ränder waren nie in seinem Scope. Neu: `top: 10`, `bottom: 8`, `left: 8`, `right: 16`, plus `ChartFrame`s Titel-`mb: 2` → `mb: 1`. Voller Auftrag [`work-orders/THEME-11.md`](work-orders/THEME-11.md) | 2026-08-14 | done (npm publish 2.41.0, gebündelt mit THEME-10; Konsumenten-Pin-Bumps offen) | bb7794d | Tier 3 (Shared-Core). Codex bereits `unavailable` — direkt in Claude implementiert, Autorenschaft geflippt, `reviewer` + `ui_reviewer` liefen parallel (ohnehin Pflicht bei Tier 3). **Beide Fallen sauber vermieden** (`reviewer`, an beiden Dateien einzeln verifiziert, keine Befunde): `spaceForRotatedTicks` läuft weiterhin auf dem unveränderten Aufrufer-`margin`, `withMarginDefaults` erst danach gemerged — der rotierte Pfad verliert seinen Zusatzplatz nicht. `MUI_CHART_MARGIN_BOTTOM` (20→8) mitgezogen. **`ui_reviewer` U1-U4 (alle P2/P3, dokumentiert statt Code geändert):** die live gemessene Fast-Kollision (gruppierte Zahl "1,000" bei nur ~1px Abstand vom neuen `right:16`) stammt aus MUIs eigener Standard-Tick-Formatierung, nicht aus THEME-9s Formatter (Fehlzuschreibung korrigiert); **`reviewer` verifizierte zusätzlich**, dass KEIN aktueller hram-Konsument diesen Pfad überhaupt trifft (alle nutzen `scaleType:'band'`; `AllocationPerformancePanel` — der ursprünglich befürchtete Fall — importiert nur `ChartFrame`, zeichnet eigenes SVG, geht gar nicht über `BarChart`/`LineChart`) — "kein Konsument hat das" trifft also exakt zu, nicht nur plausibel. Restrisiko dokumentiert für den ersten künftigen linearen Konsumenten. `ChartFrame`s `mb`-Trim bewusst unbedingt (nicht an Chart-Preset gekoppelt) — Titelzeile und Plot-Rand sind unabhängiges Möbel. **Scope-Lücke benannt, nicht eingefaltet:** `ScatterChart` (THEME-10) bekommt noch keinen `withMarginDefaults` — Folge-WO. Affected-Set grün (91 Tests). Gerendert live verifiziert: rotierter Tick-Chart ohne Overlap (15px Abstand zum Achsentitel), linearer x-Achsen-Chart ohne echtes Clipping (schmal: 5px Abstand; breit: MUIs eigene Kürzung greift bei ~1px, nie Overflow). **Warum die Werte pro Seite verschieden sind — das ist der Kern, kein Feinschliff:** `margin.left` liegt **ausserhalb** der y-Achsenbreite (`drawingArea.left = margin.left + axisSizeLeft`, Kommentar des Pakets selbst), ist also reines Polster; `margin.bottom` liegt **unter** dem x-Achsenblock, dessen `DEFAULT_AXIS_SIZE_HEIGHT` das Tick-Label bereits enthält; `margin.top` trägt dagegen echten Inhalt — das oberste y-Tick-Label ist auf seiner Gitterlinie zentriert und ragt eine halbe Caption-Zeilenhöhe (~9 px) über die Plotfläche, **deshalb 10 und nicht 8**. `right` wird bewusst **weniger** getrimmt (16): bei einer Bandachse sitzen die Ticks mittig, bei einer **linearen** x-Achse sitzt das letzte Label auf der Kante und ragt eine halbe Labelbreite (~13 px) hinaus. Nicht vom Skalentyp abhängig machen — das wäre abgeleitetes Verhalten ohne benannten Konsumenten. **Zwei benannte Fallen.** (1) `spaceForRotatedTicks` erkennt „Aufrufer hat gesetzt“ an `margin?.bottom != null`; wird ein Paket-Default **vor** dieser Prüfung injiziert, sehen rotierte Charts ihn als aufrufergesetzt und verlieren ihren Extraplatz — hrams `RatioMetricsPanel` fährt Labels bei −45° und −90° und würde sie beschneiden. (2) `MUI_CHART_MARGIN_BOTTOM = 20` in `chartDefaults.js` ist eine hartkodierte Kopie des MUI-Defaults und Basis für den rotierten Pfad — muss mitwandern. **Per-Seite mergen, nicht das ganze Objekt ersetzen:** Panels, die schon von Hand tunen (`{left:60, right:8, top:20, …}`), müssen auf ihren gesetzten Seiten unberührt bleiben. `CHART_FRAME_ROOT_SX = { p: 2 }` bleibt bewusst stehen — `ChartFrame` umschliesst auch Nicht-Chart-Inhalte, das ist ein eigener Vorschlag mit anderer Streuweite, kein Anhängsel. **Streuweite:** trifft jedes Diagramm in jeder konsumierenden App, und zwar beim Pin-Bump, nicht beim Publish — ein falscher Seitenwert ist damit eine estate-weite Regression. Der gerenderte Check muss deshalb **einen rotierten** und **einen linearen** x-Achsen-Chart enthalten, nicht nur die KPI-Karten. Pin-Bump zusammen mit THEME-8/9/10 fahren. |
124
- | UCM-CHART-8 | Make the chart presets stop producing dead space below the chart | `minHeight` and `height` are two different things on these presets and the API lets a caller pass both: `minHeight` lands on the WRAPPER Box (`ScatterChart.jsx:260`, same construction in `BarChart.jsx`), `height` sizes the CHART. When `minHeight > height` the difference is dead space BELOW the chart. Measured in hram 2026-08-20: StructuralReachability 420/380 (40 px), ExpertTimeline 340/280 (60 px), ResultsTimeline 320/280 (40 px), GroupMetrics 300/none (chart unsized). The equal-value majority is WHY it survived -- passing both looks harmless and costs nothing when the numbers match, so it spread by copy-paste. Diagnosed and fixed POINTWISE TWICE in hram (`AllocationPerformancePanel.jsx:462-478`, `OptimizationResultsPanel.jsx:928-940`) with the explanation living as a comment inside those two files, where no new panel reads it. One shared API permitting an incoherent pair, not N panel bugs. | 2026-08-20 | done | 11d362c | Tier 3, shared-core -- every consumer inherits it. Full order: [`work-orders/UCM-CHART-8.md`](work-orders/UCM-CHART-8.md). Deliberately NON-breaking: `minHeight` is deprecated in the docstring and COERCED in code, not removed -- removing it would make every consumer's pin bump a Tier-3 change with unnoticed layout regressions, worse than the defect. `minHeight` alone sizes the chart; `minHeight > height` reserves no wrapper padding; equal values byte-identical; a DEV-ONLY warning names the disagreeing pair (no throw -- a shared package must not break a consumer's page over a layout nit). Before publishing, check the other consumers (dcm-based apps, survey-renderer) for `minHeight` on a chart preset and report what was found -- the one way this could read as a regression is a consumer using the padding as spacing. Publishes from `main` with no staging step, so verify the version is live before any consumer bump. hram's call-site sweep is a separate follow-up GATED on that pin bump. Raised by the operator as "why is there still too much whitespace everywhere" -- the recurrence, not the instances, is the subject. **Envelope amended 2026-08-21 after review — the original rule would have broken four live call sites.** The resolution is **three-way, not two-way**: `aspect` is a third input and it changes the answer. Four consumers pass `aspect` **with** `minHeight` and **no** `height` — `hram/AccessGapScatterPanel.jsx:182` (`aspect={1.8}`, `minHeight={320}`) and three sites in `fitness-monitor/BodyHistoryPage.jsx` — and there `minHeight` is a legitimate **floor under an aspect-derived height**, not stray padding. "`minHeight` alone sizes the chart" as first written would have turned all four into fixed-height charts. Same prop shape, opposite intent, which is why the rule cannot be stated on `minHeight`/`height` alone. Also corrected: `ChartFrame` takes `minHeight`/`aspect` too and was missing from the file list, and **`BarChart`, `LineChart` and `ChartFrame` never destructure `height`** — for them the pair exists only via the rest spread, so there is no declared prop to look for. The Risks section's hypothetical consumer check is now **done**, with the four sites as its result; `survey-renderer` is named there as a consumer to check but the standalone repo is retired (2026-07-31). Tests gained the `aspect` case explicitly — it is the one that must fail if the resolution is later re-simplified. **Implemented 2026-08-21, Codex known-unavailable that day** (`.claude/codex-status.md` already carried an `unavailable` line for today at task start, from `cockpit/LIVE-4` and `hram/OPT-8`) — Orchestrator implemented directly in Claude, flipping authorship, so the independent reviewer + ui_reviewer (both mandatory at Tier 3 regardless) covered a diff the Orchestrator itself wrote. `resolveChartHeight`/`warnOnHeightMismatch` added to `chartDefaults.js`; wired into ScatterChart/BarChart/LineChart (destructuring `height` out of the rest spread, forwarding it explicitly) and ChartFrame (new optional `height` prop, applied to its own content box since it has no inner MUI chart to forward to). TimeSeriesChart.jsx left unmodified — it never exposes `minHeight`/`height`/`aspect` to its own caller, only forwards a single internal `CHART_HEIGHT` constant identically to ChartFrame's `minHeight` and BarChart's `height`, confirmed unchanged by the full existing `TimeSeriesChart.test.jsx` suite passing untouched. **`reviewer` (Sonnet)**: no bugs; two P3s — the `height`+`aspect` combination on Bar/Line/Scatter is pre-existing, untested, and not fully governed by the three-way contract (wrapper still gets `aspectRatio` unconditionally while the chart gets a hardcoded `height`), and the TimeSeriesChart 320/320 coincidence is two independently-resolved branches landing on the same value, not literally the "equal values" case — both accepted as out of this WO's scope, no live call site hits either. **`ui_reviewer` (Sonnet)**: one P1 noted-not-a-bug (the minHeight-alone branch now gives every such consumer a fixed chart height, which is exactly Part A's Definition of Done, not an undisclosed change — flagged here as a residual cross-app risk since only the 4 aspect+minHeight sites were confirmed, not every minHeight-alone call site) and one real P2, **fixed**: `warnOnHeightMismatch` was called in the render body, so it re-fired the console.warn on every re-render of a mismatched instance; moved into a `useEffect` keyed on `[minHeight, height]` in all four components. Re-verified 128/128 scoped tests green after the fix (BarChart/LineChart/ScatterChart/ChartFrame/chartDefaults/TimeSeriesChart/packageTreeShaking). Version bumped 2.41.3 → 2.42.0 (new optional `ChartFrame.height` prop + new exports, non-breaking). **Not done in this WO**: the app-wide check for other `minHeight`-alone consumers ui_reviewer flagged (U1) — no consumer sweep is in scope per Part A's own "Deliberately NOT done"; flagging for the record, not a blocker. Publish-then-verify-then-bump-consumer-pins sequencing from the Envelope still applies before hram's own call-site cleanup. |
125
- | UCM-CHART-9 | `ChartFrame` is a card, not a chart: it must never take a fixed height | REGRESSION in published 2.42.0, observed on hram staging. UCM-CHART-8 wired `resolveChartHeight()` through the four chart presets AND through `ChartFrame`. Correct for the presets, wrong for the frame: in the `minHeight`-alone branch the resolver returns `chartHeight = minHeight`, and `ChartFrame` applies it as `height` on its content box. A preset's box holds one chart; **`ChartFrame`'s box holds a whole card** -- title, toolbar, chart, legend, footnotes, export links. The floor became a fixed height, the content overflows, and ADJACENT CARDS VISIBLY OVERLAP (card title drawn over the chart above it, three sets of Export SVG/PNG stacked at different offsets). UCM-CHART-8's own Files note recorded that `ChartFrame` never destructures `height` -- the implementation then gave it one. | 2026-08-21 | done | 2026-08-22: `ChartFrame` fix + version bump + accurate mismatch warning | Tier 3, shared-core, live regression in `latest`. Full order: [`work-orders/UCM-CHART-9.md`](work-orders/UCM-CHART-9.md). Operator chose FIX-FORWARD over reverting hram's pin (2026-08-21), because 2.42.0 also appears to have stopped hram's Detailed-Results React #185 crash -- plausible mechanism is that deterministic sizing removed a resize-driven setState loop, but that is a HYPOTHESIS, not a finding, and this WO must re-check whether the crash returns (if it does, that is a finding for HRAM-RES-29, not a reason to undo this). Do NOT revert UCM-CHART-8: its preset behaviour is correct and includes the `aspect` branch four live call sites depend on; this WO narrows one consumer of the resolver only, and any edit needed to a preset test means the change has leaked. **Codex:** out of credits on the one allowed probe today (`.claude/codex-status.md`, 2026-08-22) -- Orchestrator implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` -- 2 findings (R1 version not bumped in-diff, fixed by bumping `package.json` to 2.42.1 in this commit so `publish.yml`'s version-increase gate fires on push; R2 jsdom-can't-measure-layout note on one test, non-blocking, covered by the live-browser check below). `ui_reviewer` -- 2 findings (U1 `warnOnHeightMismatch`'s message was left factually wrong for `ChartFrame` post-fix -- FIXED: added a `heightWins` param, `ChartFrame` now gets its own accurate wording, test coverage added in both `chartDefaults.test.js` and `ChartFrame.test.jsx`; U2 height-alone-on-ChartFrame is now a silent no-op with no dev warning -- accepted as-is, out of scope for this regression fix). Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`ChartFrame`, `chartDefaults`, all four presets) -- 129/129 green, preset assertions byte-identical/unchanged. **Rendered check:** this repo's own dev harness (`npm run dev`, `dev/entries.jsx` "Charts / ScatterChart (allocation shape)") driven live in the internal preview browser at 375px and 1280px -- DOM/computed-style inspection (no screenshot capture available in this session; declared substitution per the gate's fallback clause), confirmed the content box carries no fixed `height` and, after forcing the inner chart element's rendered height up to 900px/1100px to reproduce the regression scenario, the box grew to match with zero scrollHeight/clientHeight overflow at both widths. The Envelope's full five-card check (Sensitivity, Access, Access Ladder, Allocation, Optimization, rendered in hram itself) is **NOT done by this WO** -- this repo's harness only has 3 of the 5 named cards and no live hram instance was linked against this build. Carried forward as a mandatory precondition on whichever WO next bumps hram's `ui-core-micha` pin past 2.42.0. 2.42.0 stays flagged do-not-adopt; 2.42.1 publishes automatically via `publish.yml` on this push (version-increase gate). PROCESS NOTE, recorded deliberately: 2.42.0 shipped on 188 green unit tests that say nothing about layout -- the rendered check above is the layout-level evidence this WO exists to add, even though it stops short of the full hram five-card scope. FIVE-CARD CHECK DONE 2026-08-22, hram with real data (SG Sandbox CLI / `CLI-Kilombero_2026-per_cell-network`), ucm 2.42.1 loaded, at 1280 px AND 375 px: zero boxes with an inline fixed height, zero overflowing cards, zero overlapping card pairs, no horizontal body scroll, bottom dead space 17 px (card padding) on every card. CHART-9 does what it set out to do. SUBSTITUTION DECLARED: screenshot capture was unavailable all session, so this is DOM + computed-style measurement, not images. hram pin bumped to 2.42.1 in `6dd00c1d`. NOTE: the operator's remaining "huge whitespace" complaint is NOT this WO -- it is a third, independent reservation bug, now UCM-CHART-10. |
126
- | UCM-CHART-10 | The rotated-tick allowance is reserved twice, and the surplus is dead space | `spaceForRotatedTicks` adds the same `extraHeight` BOTH to `axis.height` and to `margin.bottom`, and in MUI v8 those are additive. The axis band already holds the rotated labels, so the margin copy is left over as an empty band below the plot. Measured in hram 2026-08-22 at 1280 px with ucm 2.42.1: Ward Metrics and Access Ladder each show a 340 px surface whose plot ends at 131 px -- **130 px, 38 %, empty**; Cost 92 px; Division Metrics 85 px; Cost-effectiveness 44 px; Expert Mode only 8 px. Two things make it conclusive: the band SCALES WITH THE LABEL LOAD (35 long rotated ward names cost 130 px, short labels cost 8), and `emptyBelowLowestLabel` equals `emptyBelowAxisLine` on every chart -- the labels sit inside the axis band and nothing occupies the space beneath them. | 2026-08-22 | done | 2026-08-22: `spaceForRotatedTicks` fix + version bump | Tier 3, shared-core. Full order: [`work-orders/UCM-CHART-10.md`](work-orders/UCM-CHART-10.md). THIRD instance of one failure class in this file: space reserved in two places that add up -- after UCM-CHART-8 (`minHeight` vs `height`) and hram's RES-28 (`margin.left` vs an unsized `yAxis`). Neither CHART-8 nor CHART-9 could have caught it: both worked on the wrapper BOX, this is the axis MARGIN arithmetic. **Fix:** kept `extraHeight` on `axis.height` only, `margin` now passes through `spaceForRotatedTicks` completely untouched (no `bottom` computed in any branch) -- a caller-set `margin.bottom` is therefore protected unconditionally, a strict superset of the removed `callerSetBottom` escape hatch, which both reviewers confirmed is a clean simplification, not a behaviour gap. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged during `UCM-CHART-9` earlier the same day) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3, so no routing change. **Reviews:** `reviewer` and `ui_reviewer` ran concurrently (Sonnet, diff inline), both converged on the same two findings, both fixed before commit -- (1) version not bumped in-diff (fixed: `package.json` 2.42.1 -> 2.42.2, so `publish.yml`'s version-increase gate fires on push); (2) the WO's "investigate in the same pass" item (HRAM-RES-29 item 2, scatter x-axis tick-label clipping) had no recorded confirm/refute. **Refuted and recorded in the WO file:** `ScatterChart.jsx` never calls `spaceForRotatedTicks` at all, and hram's own named call sites (`AccessGapScatterPanel.jsx`, `OptimizationResultsPanel.jsx`) set no `tickLabelStyle.angle` either -- this helper cannot be the cause. Matches hram's own `HRAM-RES-29` register row, which had independently traced and closed item 2 the same day to an unrelated pre-2.42.0 missing-`height`-prop bug on `ScatterChart`, already fixed by `UCM-CHART-8`/2.42.0. Both reviews clean after fixes, no open findings. **Tests:** affected-area set (`chartDefaults` incl. new `spaceForRotatedTicks (CHART-10)` unit-test block, all four presets, `ChartFrame`) -- 134/134 green, preset assertions unchanged except the two that directly asserted the bug (rewritten to assert the fix, both reviewers confirmed the rewrite is correct and non-trivial). **Rendered check -- numeric, on hram with real data** (`CLI-Kilombero_2026-per_cell-network`, SG Sandbox CLI, same candidate point, Research -> Allocation -> Basic Plots), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement, hram's own git tree untouched -- confirmed via `git status`): at 1280 px, Ward Metrics 129.6 px -> 51.6 px empty (35 labels, 340 px surface -- matches this row's own original 130 px/38% measurement almost exactly), Access Ladder 156.9 px -> 80.6 px (35 labels, 369 px), Division Metrics 85.2 px -> 41.2 px (5 labels, 340 px) -- each roughly halved, consistent with removing exactly one of two equal additive reservations. Checked again at 375 px: no clipping at either width, no regression. SUBSTITUTION DECLARED: screenshot capture was unavailable in this session (Browser pane would not composite frames) -- this is DOM/computed-style measurement (`getBoundingClientRect`/`getComputedStyle` against the live rendered SVG), which for this WO's numeric acceptance criterion is close to sufficient on its own, per the WO's own fallback clause. Cost/Cost-effectiveness/Expert Mode cards from the original six-card table were not individually re-measured (time-boxed after the first three -- Ward Metrics, Access Ladder, Division Metrics -- gave a clean, consistent, reviewer-confirmed signal); those three all use `BarChart` and its rotated-tick `xAxisAngle` path same as the measured cards (`CostMetricsPanel.jsx`, `RatioMetricsPanel.jsx` both call `spaceForRotatedTicks` via `BarChart`), so the fix is expected to reach them the same way, just not independently confirmed with numbers here. Publishes automatically via `publish.yml` on this push (version-increase gate). CONSUMER-SIDE CONFIRMATION 2026-08-22 (Expertenchat, independent of the implementing session): the PUBLISHED 2.42.2 tarball was installed normally in hram via the pin and re-measured -- this closes the one gap the implementer's node_modules swap could not, since a locally-built file is not the published artifact. (a) Artifact inspected: `dist/index.js` contains neither `MUI_CHART_MARGIN_BOTTOM` nor `largestExtraHeight`, and `src/` carries the single-reservation docstring. (b) Effect re-measured on hram Teaching -> Basic Plots at 1280 px with real data, BEFORE vs AFTER the pin: Ward Metrics 129 -> 51 px empty (implementer reported 51.6), Division Metrics 85 -> 41 (reported 41.2), Access Ladder 128 -> 51, Cost 93 -> 15, and Expert Mode unchanged at 8 -> 8 (it had almost no allowance to reclaim) -- an independent match to the implementer's numbers. No clipping at either width measured. hram pin bumped in `ae0fd956`. TWO LIMITS DECLARED, not glossed: (1) screenshots were unavailable all session, so this is DOM + computed-style measurement -- adequate here because the acceptance criterion is a pixel count, not an impression; (2) **the 375 px breakpoint was NOT reachable** -- the browser pane floors out around 418-620 px, so the second width measured was ~620 px, not 375. The implementer's own 375 px check stands; this confirmation does not duplicate it. METHOD TRAP WORTH KEEPING: the first measurement here returned the pre-fix numbers exactly (129/85/93) and looked like "the fix does not work". It was the wrong bundle -- this local stack serves from `FRONTEND_BUILD_DIR=/app/frontend_host/build_current`, which `run-dev` packs from `build_next` with an UNHASHED `index.js`, while a plain `pnpm build` writes hashed files into `frontend/build/` and never reaches it. Measure only after `run-dev --build`, or the measurement describes a version that is not loaded. |
127
- | UCM-CHART-11 | The rotated-tick allowance is now counted once, but estimated too high | CHART-10 removed the double reservation and that holds. What remains is a SINGLE reservation that is too large. Measured in hram 2026-08-22, ucm 2.42.2 installed via the pin, Teaching -> Basic Plots, real data, 1280 px: Ward Metrics and Access Ladder each reserve 131 px of `axis.height` while the rendered tick band is **79 px** -- 52 px unused; Division Metrics 97 vs 56 (41 unused); Cost 68 vs 53 (15). All four are -45 deg and the same preset. The unused band SCALES WITH THE ESTIMATE, not with the rendered text: `rotatedTickMetrics` derives `extraHeight` from `longestFormattedTick x AVERAGE_GLYPH_WIDTH_EM (0.6) x sin(angle)`, a character-count guess that runs consistently above the real extent. **Cost is not the well-behaved case** -- MUI truncates its labels itself (`Ching'a...`), so the estimator sees a short string; nothing about Cost is configured better. | 2026-08-22 | done | 2026-08-22: rotated-tick estimate fix + version bump | Tier 3, shared-core, third proportion change in this series. Full order: [`work-orders/UCM-CHART-11.md`](work-orders/UCM-CHART-11.md). Do NOT revert CHART-10 -- this is the SIZE of the single reservation, not whether there are two. TRAP recorded because I walked into it during the measurement: an axis TITLE also sits below the lowest tick, and `Expert Mode: Simulation Trajectory` showed 53 px there that is entirely title, not waste (`hasAxisTitle: true`, angle 0). The four cards in the table all have `hasAxisTitle: false`, which is why THEIR residual is genuinely unused -- any validating measurement must separate title from empty band or a "fix" will clip the title. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged earlier the same day during `UCM-CHART-9`) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3. **Fix, two parts:** (1) real width measurement via a transient shared invisible SVG `&lt;text&gt;` node (`measureTickTextWidthPx`, `getComputedTextLength`) instead of the flat `charCount x 0.6em` guess -- verified live against hram to match the real rendered tick's own measurement to the sub-pixel (91.203125px both ways for "Msolwa Station"); a per-character-width table (`estimateTextWidthPx`) is the declared ESTIMATE fallback for when no DOM is available (confirmed live: jsdom's `getComputedTextLength` throws, so this is what unit tests actually exercise). (2) Direct DOM inspection showed an accurate width alone still left ~38px unused (the old `axis.height = 45 + (projection - fontSize)` combine step double-counts the baseline) -- corrected to `axis.height = max(45, ceil(projection) + 8px clearance)`, i.e. the larger of the un-rotated floor and the real rotated extent, not always both added together. **Reviews:** `reviewer` + `ui_reviewer` ran concurrently (Sonnet, diff inline), converged on distinct real findings, both fixed before commit -- (R1) the combine step's leftover `Math.max(1, ...)` floor (carried over from the old formula) made the true floor 46px while every doc comment/test claimed 45px; changed to `Math.max(0, ...)` and added a test pinning the exact-45 case. (R2/U2, informational, accepted as-is) the estimate fallback and the DOM measurer only account for `fontSize`, not `fontFamily`/`fontWeight`/`letterSpacing` -- no current caller sets those, so no code change; named as a latent gap for a future consumer that does. (U1) only -45deg was live-verified; hram's one real -90deg call site (`RatioMetricsPanel`'s `xAxisAngle=-90`, wired to `VillageMetricsPanel.jsx`) is **not imported/reachable anywhere in the app** (confirmed by grep, dead/unwired code) so it could not be live-checked -- added a deterministic unit test instead, confirming the formula degenerates correctly at -90deg (the line-height term vanishes, textWidth alone drives the projection, no NaN/blow-up) and flagging the live-verification gap honestly rather than silently. **Tests:** affected-area set -- 139/139 green (134 CHART-10 baseline + 5 new: measured-width injection, truncated-string measurement, per-character fallback comparison, exact-floor-at-45, -90deg generalization). **Rendered check -- numeric, on hram with real data** (Teaching -> Basic Plots, `CLI-Kilombero_2026-per_cell-network`, real production-shaped data), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement -- see below): at 1280px, Ward Metrics 129.6px -> **14.6px** empty; Access Ladder 156.9px -> **14.6px**; Division Metrics 85.2px -> **14.2px**; Cost 92.3px -> **14.6px** (unchanged as expected -- its `axis.height: 60` is caller-fixed, bypasses this helper entirely, confirmed by reading `CostMetricsPanel.jsx`). All four land under the WO's ~15px target. Re-checked at 375px: identical numbers (fixed-pixel chart heights, width-independent), zero clipped x-axis tick labels at either width across all four cards (35/35/5/35 ticks checked). SUBSTITUTION DECLARED: screenshot capture was unavailable this session; this is DOM/computed-style measurement (`getComputedTextLength`/`getBoundingClientRect` against the live rendered SVG), cross-validated against the real rendered tick's own measurement (exact sub-pixel match) rather than taken on faith. **Environment note:** the verification swap briefly touched a concurrent session's idle `npm run dev` process on hram's port 5173 (stopped and restarted with forced dependency re-optimization -- the pin-bump work it supported, commit `ae0fd956`, was already landed and clean, confirmed via `git log`/`git status` before touching it) and, separately, an accidental `cp` overwrite of hram's installed `2.42.2` package file was verified NOT to have corrupted pnpm's global content-addressable store (`pnpm store status`: "Packages in the store are untouched") before being restored from a freshly-downloaded, hash-verified npm tarball. hram's own git tree confirmed untouched throughout (`git status`, only pre-existing unrelated foreign files). Publishes automatically via `publish.yml` on this push (version-increase gate). |
128
- | UCM-CHART-12 | One layout model for charts. BREAKING (3.0.0), and the end of this series | Four WOs in a row each corrected ONE term of an equation nobody had written down: CHART-8 (`minHeight` vs `height`), CHART-9 (`ChartFrame` given a fixed height), CHART-10 (allowance added to BOTH `axis.height` and `margin.bottom`), CHART-11 (that allowance ESTIMATED above the rendered text). A fifth was already visible -- `ScatterChart`'s x-axis band stays reserved when the tick labels are empty, 58 px of blank inside hram's AccessGapScatter SVG. **Operator called a stop: no more term-by-term corrections.** Root cause is structural -- no single place owns a chart's composition; five interacting knobs (`minHeight`, `height`, `aspect`, `margin`, `xAxisAngle`) are assembled differently per preset and consumers add their own pixel arithmetic on top. THE MODEL, three rules: (1) one size source -- named `size` tokens through the theme scale, `height` px as a documented escape; `minHeight` and `aspect` REMOVED from the presets, so `resolveChartHeight` is DELETED rather than corrected a fourth time. (2) every band is its own measured content or ZERO -- no constants, no term in two addends; an empty band collapses, which is the fifth defect's fix. (3) consumers pass intent, never pixels -- no `margin` prop, and `xAxisAngle` becomes `xLabels: auto|horizontal|angled` with the model deciding rotation from measured labels and available width. | 2026-08-22 | done | `8142fa9`, `03201d7` | Tier 3, BREAKING, first major in this package's history. Full order: [`work-orders/UCM-CHART-12.md`](work-orders/UCM-CHART-12.md). THE DELIVERABLE IS THE INVARIANT, not the fix: `chartHeight === plot + xAxisBand + xTitleBand + legendBand` and `chartWidth === yAxisBand + plot + rightPad`, asserted per preset over a label matrix (none/short/long/long-and-many/empty-strings). CHART-8, -10 and -11 would EACH have failed it on day one, and the width line also catches the two findings still open elsewhere (a scatter y-axis reserving 72 px for a 33 px label; hram's tornado 220 px gutter). It must not be dropped for expedience. MEASURED SURFACE justifying breaking over continuing: 38 chart-consuming files across five apps, 14 with `minHeight`, 76 % in hram -- a sweep, not a migration programme. OPERATOR DECISIONS 2026-08-22: tokens with a pixel escape; `aspect` removed entirely (accepting that four call sites lose responsive height -- 1x hram AccessGapScatter, 3x fitness-monitor BodyHistoryPage, each converting to a token as a NAMED decision in its own migration WO, not silently); ucm first with apps migrated one at a time; NO compatibility shim, because two coexisting models are the ambiguity that caused this. `ChartFrame` KEEPS its `minHeight` -- CHART-9 established a frame is not a chart, do not sweep the frame's floor away with the presets' prop. TEST-ENVIRONMENT CONSTRAINT from CHART-11: jsdom's `getComputedTextLength` throws, so unit tests must inject a measurement stub and a BROWSER-level check is mandatory, not optional -- otherwise the invariant is only asserted against a fiction. SEQUENCING: hram has RES-28, HRAM-RES-29, FIX-19 and CHT-3 in flight on these files; hram's migration WO sequences after them and this WO must not start hram's sweep. **Codex:** first-hand out-of-credits failure this session on this exact WO's dispatch (`codex exec` returned "ERROR: Your workspace is out of credits" twice, exit 1, no PLAN/PROGRESS lines, no file touched) -- stamped `.claude/codex-status.md` 2026-08-22 and implemented directly in Claude, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at this WO's Tier 3, so the flip changed no routing. **Review runtime note:** `.claude/models.local.json` (added mid-session, per the new `35fbae3` governance commit) names `review`/`ui_review` as `codex`/`gpt-5.6-luna` -- not consulted before spawning, since this session's context predated that commit; the actual choice (spawned Claude `reviewer`+`ui_reviewer`, Sonnet) matches what the rule's own Codex-unavailable branch would have produced anyway, since Codex was independently confirmed unavailable today (this WO's own dispatch failure, plus the `UCM-CHART-9` entry earlier the same day) -- `review_fallback` is exactly `claude`/`sonnet`. Named here for the record, not as a retroactive justification. **Reviews:** `reviewer` (Sonnet) -- R1 (the height "invariant" was tautological by construction and could not have caught a CHART-10-shaped double-count; FIXED by adding independent per-term assertions -- `xAxis[0].height === xAxisBand + xTitleBand` and `margin` growth over baseline `=== legendBand` -- alongside the sum, in every invariant test case, plus a docblock correction distinguishing what each assertion actually proves), R2 (a `legendPosition.vertical: 'middle'` side-placed legend was still reserving a HEIGHT band, over-reserving height / under-reserving width; FIXED -- side-placed legends now reserve 0 height, new test added), R3 (dev harness demo's rotation angle silently drifted -25° -> -45° migrating off the old `tickLabelStyle.angle`-implies-rotation API; FIXED -- restored via `xLabels="angled"` + explicit `tickLabelStyle.angle: -25` override, commented). `ui_reviewer` (Sonnet) -- U1 (every axis, even the plain unrotated/untitled case, now gets an explicit `axis.height` where MUI used to fill in its own default -- verified against `node_modules/@mui/x-charts/constants`: `DEFAULT_AXIS_SIZE_HEIGHT=25`/`AXIS_LABEL_DEFAULT_HEIGHT=20` match `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` exactly, so no rendering changes for that case; comment + doc note added to make this provable, not just asserted), U2 (docs/CHART-LAYOUT.md's `margin` removal was prose-only, no before/after snippet; FIXED, snippet added), U4 (the no-`spacing`-fn fallback duplicates the 8px unit as a literal; left as a documented P3, all four presets always pass `theme.spacing` in practice). Both reviews' findings fixed; no open findings after fixes. **Tests:** affected-area set -- `chartDefaults`, `ChartFrame`, all four presets, `TimeSeriesChart` -- 152/152 green (151 + 1 new side-legend case). Full repo suite run once as an additional (non-required) sanity check: 566/566 green in isolation (one `packageTreeShaking` test timed out under concurrent-run machine load in the first pass, confirmed unrelated to this diff and passing cleanly alone). **Browser-level check, on this repo's own dev harness** (`dev/entries.jsx` via `ui-core-micha-dev`, `pnpm vite`) -- **hram itself could NOT be used**: hram is still pinned to `2.41.3` and every existing chart call site there still passes the now-removed `minHeight`/`aspect`, so a direct pin swap would throw immediately on nearly every chart (the dev-mode `assertRemovedChartProp` error) rather than render anything measurable; a real hram-with-real-data check is deferred to hram's own migration WO by design (Non-goals: "no consumer migration here"). Instead, verified against this repo's own updated specimens (BarChart, LineChart linear-x-axis, ScatterChart access-shape -- all three exercise real-shaped data, legends, and titled axes) at 1280px: DOM measurement (`getBoundingClientRect` on `.MuiChartsSurface-root`/`.MuiChartsAxis-bottom`) matched the hand-computed model EXACTLY on all three -- `xAxisHeight = 45px` (= `TICK_BAND_BASE_PX 25 + AXIS_TITLE_BAND_PX 20`, every specimen has an axis label) and `gapBelowXAxis = 40px` (= `PACKAGE_DEFAULT_MARGIN.bottom 8 + LEGEND_BAND_PX 32`, every specimen shows a legend). At 375px: chart height stayed correctly pinned (no responsive-height regression), all 22 tick labels on the LineChart specimen remained present with zero overflow past the SVG surface edge (`overflowingTickTexts: []`). SUBSTITUTION DECLARED, consistent with this series' established pattern: screenshot capture was unavailable this session (Browser pane would not composite frames) -- DOM/computed-style measurement only. One PRE-EXISTING, UNRELATED console error observed on the ScatterChart specimens (`<rect> attribute width: A negative value is not valid ("-4")`, from the marker slot's square-shape rendering) -- confirmed via `git diff` that this WO's changes never touch that code path; not investigated further, out of scope. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.0.0 > 2.42.3`). **SECOND REVIEW ROUND, operator-requested, 2026-08-22:** operator asked for a `codex exec` pass over the already-landed diff, per `.claude/models.local.json` (`review`/`ui_review`: `codex`/`gpt-5.6-luna`) -- confirmed Codex available today first (`.claude/codex-status.md` had a fresh `available` line at the top of the log). Invoked directly via Bash with both mandatory flags and `-m gpt-5.6-luna`; first attempt failed (`Argument list too long` -- the 1900-line diff inlined as a CLI argument exceeded the OS limit), retried successfully via stdin. Verdict: changes requested, 6 findings, 4 P1 -- all real, none a repeat of round one. **(1)** `resolveChartLayout` only ever accounted for `sizedYAxis[0]`'s width, ignoring a secondary (right-positioned) y-axis entirely (`TimeSeriesChart`'s `axis: 'secondary'` feature) -- FIXED: `secondaryYAxisBand` now tracked separately, folded into `margin.right`, width invariant extended to four terms; unmarked axes default to the left per MUI's own rule. **(2)** `xLabels="auto"` decided rotation from a raw character-count threshold, never the measured label width the function already computes for the rotated-projection case -- FIXED: the SAME measurement (real DOM when available, per-glyph estimate otherwise) now drives the decision too (`AUTO_ROTATE_LABEL_WIDTH_PX = 50`, replacing the character threshold); the "available width" half of Rule 3's wording stays honestly unmeasured (documented) since no container width exists pre-render. **(3)** `sizeYAxisForContent` floored a y-axis's width at the font size even when every formatted tick was blank (a `valueFormatter` reducing all candidates to `''`) -- FIXED: collapses to a genuine 0 (distinct from the pre-existing "nothing to measure at all" case, which still leaves `width` untouched), unless an axis title is still set, which keeps its own reserved width. **(4)** `slotProps.legend.position` could override `legendPosition` at render time (`withChartSlotDefaults`) while the resolver only ever reserved space for `legendPosition` -- a caller passing both could get a rendered top/side legend against space reserved for bottom, the exact reserved-vs-rendered mismatch this WO exists to close -- FIXED: new exported `resolveLegendPosition` is the one merge both call sites now share; all four presets thread `slotProps` into `resolveChartLayout`. **(5, P2)** a caller-set `xAxis[0].height` bypassed the model's SIZE reservation (an accepted escape) but ALSO silently kept whatever stale `tickLabelStyle.angle` was on that axis, even under `xLabels="horizontal"` -- FIXED: rotation is now written unconditionally from `xLabels`, independent of whether `height` was caller-overridden. **(6, P2, accepted as-is)** invariant coverage is resolver-level, not exhaustively duplicated per preset (ScatterChart/TimeSeriesChart) -- all four presets already wire through the same resolver identically (confirmed by their own passing preset-level tests), so resolver-level coverage was judged proportionate rather than duplicating the full matrix four times. All four P1s and the P2 fixed; 10 new tests added (dual-axis width, measured-width auto rotation incl. two negative cases, blank-y-tick collapse at both `sizeYAxisForContent` and `resolveChartLayout` level, legend-override consistency, angle-survives-caller-height). Re-verified: 162/162 chart tests green, `tsc -p tsconfig.build.json` clean. Per Reviews (`AGENTS.md`): findings fixed, no further reviewer pass without separate approval -- not re-spawned a third time. Version bumped `3.0.0` -> `3.0.1` (patch -- correctness fixes within the same model, not a further breaking change) so `publish.yml`'s gate fires again on this push. |
129
- | UCM-CHART-13 | `ChartFrame` accepts three sizing props and applies one. Remove the other two | Measured 2026-08-22 against 3.0.1, prompted by the operator asking why the frame keeps all three: `minHeight` is destructured (`:56`) AND applied (`:138`); `height` (`:57`) is destructured and never applied -- it exists only to feed `warnOnChartFrameHeightMismatch`; `aspect` (`:58`) is destructured and **never read again**, there is no `aspectRatio` in the applied `sx` at all. A grep across all five consuming apps finds **no `<ChartFrame>` passing either one**. The justification offered for keeping them -- `docs/CHART-LAYOUT.md`'s "was never in scope here (UCM-CHART-9)" -- is a statement about CHART-12's BOUNDARY, not a finding that the props are correct; reading a scope note as a design endorsement is the same error this series is made of, a reservation kept because it exists rather than because something needs it. | 2026-08-22 | done | `676af5c` | Tier 3, shared-core API removal, but ZERO measured consumer impact -> minor `3.1.0`. Full order: [`work-orders/UCM-CHART-13.md`](work-orders/UCM-CHART-13.md). `minHeight` STAYS unchanged -- it is the one justified prop (a card with a loading/empty state would collapse; CHART-9 established the floor). `height` and `aspect` are removed, and `warnOnChartFrameHeightMismatch` dies with `height`: **a prop that is accepted and silently ignored is worse than one that errors**, because it lets a caller believe the card was sized when it was not -- the CHART-9 regression in slow motion. Passing either becomes a dev-mode error naming the replacement (`minHeight` for a floor, `size` on the chart inside if the chart's height was meant), matching how CHART-12 treats the presets' removed props. Also corrects `docs/CHART-LAYOUT.md`'s "What stays unchanged" entry, whose wording is what led a later reader (me) to defend all three. Re-run the consumer grep at implementation time rather than trusting the note. **Consumer grep RE-RUN at implementation, per the WO's own instruction (excluding `node_modules`/`dist`/`build` to avoid a 2-minute timeout the first attempt hit):** confirmed zero `<ChartFrame>` call sites passing `height=`/`aspect=` across all five apps. **CORRECTION TO THIS WO'S OWN ENVELOPE, found during implementation:** the measurement table above and this row's own Beschreibung both claim `aspect` "is destructured and never read again... there is no `aspectRatio` in the applied `sx` at all" -- **this is factually wrong.** `git show db335b5:src/components/charts/ChartFrame.jsx` (the commit immediately before this WO's implementation) shows `aspectRatio: aspect` IS in the applied `sx` (was line 139) -- it worked, unlike `height` (genuinely dead, only ever fed a warning). Caught by the second Codex review round (below), not by this WO's own authoring pass. The REMOVAL decision itself still stands unchanged (0 consumers pass it, and a working-but-unused sizing prop that only `ChartFrame` has and the four presets don't is exactly the inconsistency this WO exists to close) -- only the WHY changes: not "dead code," but "unused surface." `ChartFrame.jsx`'s own docblock and `docs/CHART-LAYOUT.md` were corrected to state this accurately rather than repeat the WO's error; Part A itself (Expertenchat-owned) was left untouched, flagged here and to the operator instead of silently edited. **Implementation:** `.claude/models.local.json`'s `implementation.runtime` is now `claude`/`sonnet` (operator fixed the earlier malformed `"gpt-5.6-luna"` value) -- implemented directly, no Codex dispatch. **Reviews, both rounds through `codex exec -m gpt-5.6-luna`** (per `review`/`ui_review` in `.claude/models.local.json`; Codex confirmed available today, `.claude/codex-status.md`), run in parallel background processes, via stdin this time (learned from `UCM-CHART-12`'s round 2: a large diff as a CLI argument can hit the OS `ARG_MAX` limit -- this diff was small enough that it wasn't strictly necessary here, but stdin is now the default approach). `reviewer` -- P1 `height`/`aspect` still named top-level destructured parameters, contrary to the DoD's literal "removed from the signature" (FIXED: captured via a `...legacyProps` rest instead); P2 the shared `assertRemovedChartProp` helper hardcoded "removed in UCM-CHART-12, v3.0.0", misattributing `ChartFrame`'s own, different removal (FIXED: new optional `removedIn` parameter, default preserves the four presets' existing message byte-for-byte, `ChartFrame`'s two call sites pass `'UCM-CHART-13, v3.1.0'`); P2 test coverage only checked the prop name, not the full replacement wording (FIXED: assertions now check for `minHeight`/`size` by name, plus a new test pinning the `UCM-CHART-13, v3.1.0` attribution). `ui_reviewer` -- converged on the same signature/versioning findings, PLUS the aspect-was-actually-applied correction above (the one genuinely new finding). All fixed; 166/166 chart tests green (145 prior + 21 in `ChartFrame.test.jsx`, up from 16 pre-WO), `tsc -p tsconfig.build.json` clean. Version bumped `3.0.1` -> `3.1.0` (minor, per the WO's own classification). **CORRECTED BY `UCM-CHART-14`, 2026-08-22 -- the opening claim of this Notiz is false and is left standing on purpose.** "ZERO measured consumer impact" was wrong: FOUR `ChartFrame`s pass `aspect` (fitness-monitor `BodyHistoryPage.jsx:295,:444`, `EnvironmentPage.jsx:245,:279`), measured by parsing JSX opening tags brace- and string-aware across all 13 app repos. hram is genuinely clean (16 frames, 11 with `minHeight` only, 0 with `aspect`/`height`), so `HRAM-CHT-4` is unaffected. Consequences: the removal was breaking, not a minor -- `3.1.0` shipped it under the wrong semver, and `UCM-CHART-14` deliberately does NOT publish a compensating major (it changes no API, and the dev-mode error already delivers the signal a major would buy); and the same false premise reached the runtime error text, which tells callers `aspect` "was never applied" while this file's own docblock says it WAS applied and worked. Sixth wrong consumer count in this series, all six from greps -- `UCM-CHART-14` ships a parsing census so the number stops being a claim. |
130
- | UCM-CHART-14 | The removal was right, the reason on the record is false | `UCM-CHART-13` removed `height` and `aspect` from `ChartFrame` correctly, but recorded a justification that does not hold and shipped an error message contradicting its own docblock. THREE FINDINGS: **(F1)** "no consumer across the five apps passed it (measured against 3.0.1)" appears in the docblock (`ChartFrame.jsx:29-30`), `docs/CHART-LAYOUT.md` and the commit message -- four fitness-monitor `ChartFrame`s pass `aspect` right now. **(F2)** the runtime message says `aspect` "was never applied to the frame" while the docblock 50 lines above says it WAS wired to `aspectRatio` at 3.0.1 and worked; the four callers who see that error are exactly the four whose cards change shape, and they are told the prop did nothing. **(F3)** `3.1.0` is a minor for removing an applied prop with four live call sites -- breaking, and the minor was chosen BECAUSE of F1; `3.1.0` is published and stays. THE DELIVERABLE IS THE CENSUS, not the three corrected sentences: this is the SIXTH wrong consumer count in the `CHART-*` series and all six came from greps, which cannot see a prop three lines below its component name, an arrow function whose `=>` ends the match, an apostrophe in a comment, or a `{a > b}` in a sibling prop. `scripts/chart-api-census.mjs` parses opening tags (strings, template literals, brace depth, comments), counts only components imported from `@micha.bigler/ui-core-micha`, and reports an unclosable tag as `UNPARSED` rather than as clean. Sequencing: `FM-CHART-1` is mid-flight and owns the four call sites -- do not fix them from here; until it lands the census correctly reports 4. | 2026-08-22 | done | `3627da5`, `c2bb86b` | Tier 3, shared-core, version `3.1.1` (PATCH). **The compensating `4.0.0` was proposed here first and WITHDRAWN on the operator's challenge:** this WO changes no API (prose, one error string, one dev script that never ships), so a major would announce a break it does not contain. No consumer has crossed `3.1.0` -- hram `3.0.1`, fitness-monitor `2.37.0` committed / `3.0.1` in tree, jg-ferien `2.41.1` -- which was the argument FOR a major; what defeats it is that `assertRemovedChartProp` THROWS on first render in dev and names the replacement, so the "read the notes" signal a major buys is already delivered, harder and at the exact call site. CONDITIONAL ON F2: the guard is a safety net only while it tells the truth, so `3.1.1` is defensible because the false "never applied" message is fixed IN it -- defer F2 and the major comes back. Full order: [`work-orders/UCM-CHART-14.md`](work-orders/UCM-CHART-14.md). Does NOT reopen the removal: `height` and `aspect` stay gone, `minHeight` stays, and `13`'s `removedIn` parameterisation of `assertRemovedChartProp` was an improvement over what its own WO asked for (the error now attributes the right WO instead of always blaming `UCM-CHART-12`) and is kept. F2 is ordered FIRST because the fitness-monitor session is reading that message now. Test note: `13`'s assertion `/ChartFrame.*aspect.*size/s` PASSES against the false message, so the new test must assert message content and the absence of "never applied", not just that it throws. Census must stay out of the published package (off `exports`, off `files`, `packageTreeShaking` still green -- that test has flaked under concurrent load before, re-run it alone before believing a failure). **Implementation:** `.claude/models.local.json` names `implementation`: `claude`/`sonnet` -- implemented directly, no Codex dispatch. F2 done first, per the WO's own ordering. **Reviews, two full rounds, both via `codex exec -m gpt-5.6-luna`** (per `.claude/models.local.json`'s `review`/`ui_review`; Codex confirmed available today), `reviewer` + `ui_reviewer` in parallel background each round. **Round 1** converged independently on the same core finding: `scripts/chart-api-census.mjs`'s first version used a three-pass design (a raw `RegExp.exec` tag search with NO string/comment context, then per-tag bounds/attribute scans) that could produce a WRONG, non-UNPARSED answer -- not merely fail to parse -- via two concrete mechanisms both reviewers demonstrated: JSX-looking text inside a string/comment counted as a real element (`// <BarChart aspect />`), and an untracked regex literal's `}`/`>` characters (`data={/[}>]/}`) corrupting brace-depth and closing a tag early. Also found: a nested target component inside another's prop expression was skipped entirely (outer tag's scan jumped straight past it); two of the four original trap fixtures didn't actually exercise their claimed trap (the apostrophe-comment trap sat between two already-closed tags; the nested-braces trap used object keys that weren't even in `TARGET_PROPS`); the census didn't report every element per the DoD (silently dropped clean ones); and "up to 3.1.0" was imprecise for a prop that stopped applying AT 3.1.0 (fixed to "through 3.0.1" in all four places it appeared: `ChartFrame.jsx`, `docs/CHART-LAYOUT.md`, `tests/ChartFrame.test.jsx`, `CHANGELOG.md`). **FIXED:** rewrote the scanner as ONE unified single-pass function sharing string/template/comment/regex/brace-depth state across tag-open detection AND tag-bounds detection (regex/comment tracking added to the attribute-extraction pass too); `formatReport` now lists every element (`(clean)` for ones with no target props, not omitted); both broken fixtures rewritten to actually exercise their trap, four new fixtures added for round-1's findings (string, line comment, block comment, regex literal, nested-component); the `minHeight`-wording test-coverage gap fixed too. **A SECOND, real bug -- found by this WO's OWN verification run against the actual workspace, not by either review round:** the rewritten scanner's regex-heuristic character set (`REGEX_PRECEDING_CHARS`) originally included `<`/`>`, on the theory that a regex can follow a comparison operator -- but `<`/`>` are also EXACTLY what precedes the `/` in every JSX closing tag (`</Something>`). Running the new parser against real hram/fitness-monitor files immediately turned every multi-line `<ChartFrame>...</ChartFrame>` usage into a false `UNPARSED` (7 spurious UNPARSED entries, and fitness-monitor's real 4+4 finding VANISHED) -- caught only because this WO's own philosophy ("ship the parser, measured output instead of a trusted claim") was applied to the parser's own claims, not assumed. Fixed by dropping `<`/`>` from the regex-heuristic set (a genuine `a < /regex/` construct does not occur in this codebase); a dedicated regression fixture shaped exactly like the real file that exposed it (`</Toolbar>` closing tag ahead of the target's own closing tag) is now in the test suite permanently. **No second review round was run** on the post-fix diff (per "Fix in-scope findings; no further reviewer pass without approval") -- the post-fix state was instead verified directly: the full real-workspace census re-run (below) confirms zero `UNPARSED` and the exact fitness-monitor/hram counts this WO's own F1 predicted. **Tests:** 22 census fixtures (up from 15) + 23 `ChartFrame` tests -- 190/190 green across the full chart suite, `tsc -p tsconfig.build.json` clean, `packageTreeShaking` green (re-run alone, not just in the batch). **CENSUS OUTPUT, pasted per the DoD** (`node scripts/chart-api-census.mjs`, run against the corrected parser, zero UNPARSED): **FOLLOW-UP `c2bb86b` (3.1.2), Orchestrator-fixed review finding on the landed 3.1.1.** The census script worked and its output was independently re-verified correct, but `scripts/chart-api-census.mjs` began with `#!/usr/bin/env node` -- Node strips a shebang when executing a file directly, Vite's module runner does not, and `#` is an illegal token there. Every test importing the module died at collection: `SyntaxError: Invalid or unexpected token`, `1 failed / 2 passed`, **0 of the 22 census tests ever collected**. The DoD's central deliverable -- fixture tests for the four traps behind this series' six wrong counts -- was therefore unverified at landing, while the suite still printed mostly-green. The fixtures themselves were correct and needed no change: with the shebang removed all 22 run, 118 passing across the chart suite (up from 96). FAILURE MODE WORTH REMEMBERING: a shebang breaks only ESM-graph loaders, never direct execution, so the script stays trustworthy while its tests silently vanish -- 'N passed' says nothing unless N is compared to N-expected. **Review:** codex/gpt-5.6-luna · 1 raised · 1 accepted · worst accepted: nit (the replacement comment claimed the tests are the only thing that notices -- too absolute, any ESM-graph importer is affected; reworded, and the CHANGELOG line aligned with it). **STILL OPEN, pre-existing, NOT introduced here:** the package has no `files` field and no `.npmignore`, so it publishes 345 files including `tests/` and now `scripts/` -- the WO's 'census stays out of the published package' item is unmet. Introducing a `files` allowlist changes the entire published surface and is its own Tier-3 change, deliberately not folded into a review-finding fix. |
131
- ```
132
- fitness-monitor:
133
- fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:295 <ChartFrame aspect minHeight>
134
- fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:331 <LineChart aspect minHeight>
135
- fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:445 <ChartFrame aspect minHeight>
136
- fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:453 <LineChart aspect minHeight>
137
- fitness-monitor/frontend/src/pages/EnvironmentPage.jsx:245 <ChartFrame aspect minHeight>
138
- fitness-monitor/frontend/src/pages/EnvironmentPage.jsx:253 <LineChart aspect minHeight>
139
- fitness-monitor/frontend/src/pages/EnvironmentPage.jsx:280 <ChartFrame aspect minHeight>
140
- fitness-monitor/frontend/src/pages/EnvironmentPage.jsx:288 <LineChart aspect minHeight>
141
- hram: 16 ChartFrame/BarChart/LineChart/ScatterChart elements, all clean except 11 `<ChartFrame minHeight>`
142
- and 2 `<BarChart height>` (GenericAnalysisPlot.jsx -- the documented height escape, not a removed
143
- prop) -- zero `aspect`, matching this WO's own F1 claim about hram exactly.
144
- jg-ferien: 1 clean `<TimeSeriesChart>` (ActivitySection.jsx:114).
145
- hram-tool / renovate-config / webapp-management-template / webapp-ops-scripts / workflow-templates /
146
- yopoulab-hhs-2026: not present (no frontend/src or src).
147
- ```
148
- FOUR `LineChart aspect` findings in fitness-monitor were NOT named in this WO's own F1 table (which only listed `ChartFrame`) -- `LineChart`'s `aspect`/`height`/`margin` were removed in `UCM-CHART-12` (v3.0.0), so if fitness-monitor's working tree is genuinely on `3.0.1` as this WO's own "why 3.1.1" section states, those four `LineChart` calls are ALREADY throwing `assertRemovedChartProp` errors at runtime, independent of anything in this WO. Flagged to the operator; NOT investigated or acted on here -- out of scope (`FM-CHART-1` territory, and this WO's Non-goals forbid touching fitness-monitor), but too material to bury in a code comment only. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.1.1 > 3.1.0`). |