From 4de307bd65e73a3d4de71c5bc44fc2a5d58f7110 Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Thu, 1 Oct 2026 13:06:33 +0100 Subject: [PATCH 1/2] feat(platform): add notifications-category-sync skill Repo-agnostic workflow for syncing a client with the backend notifications category manifest, with a MetaMask Mobile overlay. --- CHANGELOG.md | 1 + .../repos/metamask-mobile.md | 49 ++++++++++++++++ .../notifications-category-sync/skill.md | 58 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md create mode 100644 domains/platform/skills/notifications-category-sync/skill.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dbdcab5..4038b401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `platform/notifications-category-sync`: a repo-agnostic workflow for syncing a client with the backend notifications category manifest (fallback snapshot, preference keys, settings rows), with a MetaMask Mobile overlay. - Add `performance/profiling-regression-proposal`: an evidence-only skill that proposes follow-up actions after MetaMask Mobile CI has already classified a Hermes CPU-profile regression. - Support explicit-only workflow skills through native invocation controls, preserve repository overlays, and prune managed retired skill names during sync. - Distribute Perps static review as a shared execution checklist with repository overlays, source-tracked client rules and per-rule evidence outcomes, without a duplicate template catalog. diff --git a/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md b/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md new file mode 100644 index 00000000..bb6a955a --- /dev/null +++ b/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md @@ -0,0 +1,49 @@ +--- +repo: metamask-mobile +parent: notifications-category-sync +--- + +# Notifications Category Sync — MetaMask Mobile + +## Where things live + +| Concern | Path | +|---------|------| +| Fallback snapshot + `resolveNotificationCategories` (mobile filter, empty → fallback) | `app/util/notifications/categories/notification-categories-api.ts` | +| AUS key → i18n stem (`AUS_KEY_TO_I18N_STEM`) | `app/util/notifications/categories/notification-categories-i18n.ts` | +| Flag gate (`socialAI`) and in-app inbox filtering | `app/util/notifications/categories/notifications-settings-types.ts` | +| Section registry (`NOTIFICATION_SETTINGS_SECTIONS`, slugs, deeplink resolver) | `app/components/Views/Settings/NotificationsSettings/notificationSettingsSections.ts` | +| Settings rows + unsupported-category `Logger.error` | `app/components/Views/Settings/NotificationsSettings/index.tsx` | +| Per-section detail maps (`SETTINGS_TYPE_BY_SECTION`, layout) | `app/components/Views/Settings/NotificationsSettings/NotificationSettingsSectionContent.tsx` | +| Preference key type (`NotificationPreferenceSection`) | `app/util/notifications/hooks/useNotificationStoragePreferences.ts` (derived from `@metamask/authenticated-user-storage`; `agenticCli` is made required via Omit/Required) | +| Tabs (testID from `categoryTestID(category_id)`) | `app/components/Views/Notifications/NotificationsCategory/` | +| Startup fetch | `useFetchNotificationCategoriesEffect` in `app/util/notifications/hooks/useStartupNotificationsEffect.ts` | + +## Step details + +1. **Diff** + ```bash + curl -s https://notification.api.cx.metamask.io/api/v4/notifications/categories \ + | jq -S 'map(.visible_on |= sort | .aus_keys |= sort | .notification_types |= sort)' + ``` + Compare with `FALLBACK_NOTIFICATION_CATEGORIES` and update `notification-categories-api.test.ts`. If every new category has empty `aus_keys`, skip to Verify. +3. **Package**: check the key exists in the installed `@metamask/authenticated-user-storage` `NotificationPreferences` (`package.json`). +4. **Copy**: add the key to `AUS_KEY_TO_I18N_STEM`; add `app_settings.notifications_opts._title` and `_desc` to `locales/languages/en.json` only. Other locale files are updated by the translation process, so leave them untouched. Extend `notification-categories-i18n.test.ts`. +5. **Registry**: add the kebab slug to `NotificationSettingsSectionSlug` and an entry to `NOTIFICATION_SETTINGS_SECTIONS` (`slug`, `type` = AUS key, `titleKey`, `descriptionKey`, a valid `IconName`, `showStatus`, `requiresSocialLeaderboard` only if flag-gated). Add the key to the maps in `NotificationSettingsSectionContent.tsx`; both are `Record`, so `yarn lint:tsc` flags a miss. Update `notificationSettingsSections.test.ts` and the section content tests. +6. **Touchpoints**: grep `priceAlerts|price_alerts|price-alerts|agenticCli` across `app docs tests locales` (skip `tests/coverage`). Known hits: + - `tests/api-mocking/mock-responses/defaults/user-storage.ts` (AUS preferences mock) + - `docs/readme/deeplinking.md` (section slug list and the `notifications-settings` row) and `handleNotificationsSettingsUrl` tests + - `featureNotificationsGateConfig.ts` and `notifications.feature_gate..*` locale keys, only if the feature uses the gate + - flag gate in `getNotificationsSettingsSectionConfigs` if the category is flag-gated + - new `notification_types` needing inbox rendering: `notification-states/` and `TRIGGER_TYPES` from `@metamask/notification-services-controller` +7. **Tests**: `NotificationsSettings/index.test.tsx` and `NotificationsSettings.view.test.tsx`; the unsupported log uses `Logger.error` from `app/util/Logger`. + +## Verify + +```bash +yarn jest app/util/notifications app/components/Views/Settings/NotificationsSettings app/components/Views/Notifications +yarn lint:tsc +yarn lint +``` + +User-facing changes need a CHANGELOG entry (see the `pr-changelog` skill). diff --git a/domains/platform/skills/notifications-category-sync/skill.md b/domains/platform/skills/notifications-category-sync/skill.md new file mode 100644 index 00000000..f8b234a9 --- /dev/null +++ b/domains/platform/skills/notifications-category-sync/skill.md @@ -0,0 +1,58 @@ +--- +name: notifications-category-sync +description: >- + Sync a client with the backend-driven notifications category manifest when the + backend adds or changes a notification category, notification type, or + preference (AUS) key. Use when asked to support a new notification category or + type, add a notifications settings row, refresh the category fallback + snapshot, or when logs report an unsupported notification category. +maturity: stable +--- + +# Notifications Category Sync + +The notifications backend publishes a category manifest (`GET /api/v4/notifications/categories`, unauthenticated, same for every user). Clients render tabs and settings rows from it, but keep presentation (copy, icon, slug, detail screen) locally, keyed by the user-storage preference key (`aus_keys`). This skill closes the gap when the manifest gains or changes entries. + +## When to use + +- Backend added, removed, renamed or re-ordered a category, or changed its `aus_keys`, `notification_types` or `visible_on` +- A new preference key must get a settings row +- The local fallback snapshot no longer matches the live response +- Logs report an unsupported notification category + +Out of scope: rendering a new notification type's body or push payload (notification-state mapping), and gating a feature behind notifications. + +## Manifest contract + +Each entry: `category_id`, `aus_keys[]`, `notification_types[]`, `visible_on[]`. + +- Array order is display order. +- A client renders only entries whose `visible_on` includes its platform. Empty `visible_on` means hidden on every platform but still present in the manifest. +- `aus_keys` are the toggle pivot. Non-empty with no local section is a client bug: log the error once and hide the row. Empty means display-only (inbox tab and filtering, no settings row, no error). +- `category_id` is backend-owned and may not match local naming. Resolve local sections and copy through `aus_keys`, never `category_id`. +- `notification_types` is advisory; notifications carry a server-resolved `category` (`''` means uncategorized). +- The fallback snapshot mirrors the live response 1:1, hidden entries included, same order. It is used only when the request fails. + +## Workflow + +1. **Diff live against the fallback.** Fetch the endpoint, normalize (sort keys and arrays), compare with the client's fallback snapshot. List added, removed and changed categories. +2. **Classify each change.** + - New entry with empty `aus_keys`: display-only. Update the snapshot and its test, then verify. + - New entry with an `aus_key` the client already supports: update the snapshot only. + - New `aus_key`: full local support (steps 3-6). + - Changed `visible_on`, `notification_types` or order: snapshot and tests only. +3. **Confirm the preference key exists** in the client's preference types. If the installed storage package lacks it, stop and report; a dependency bump is a separate decision. +4. **Add presentation for the key**: copy (title and description, in the source locale; other locales follow the repo's translation process), icon, deeplink slug, whether the row shows channel status, and any feature-flag gate. +5. **Register the section** in the local registry and every per-key map the compiler flags (analytics settings type, detail-screen layout). +6. **Mirror per-key touchpoints.** Grep an existing key in all spellings (camelCase, snake_case, kebab-case) and update every per-category list: deeplink docs and tests, API mocks, feature-gate config. +7. **Update the fallback snapshot and tests**: snapshot mirror, settings-row rendering, the unsupported-category log (called once for non-empty `aus_keys` with no section, never for empty), and display-only skipping. +8. **Verify.** Re-run step 1 (no diff left), then the repo's unit tests, type check and lint. + +Do not guess a multi-key category's copy or icon; ask. Do not bump dependencies silently. + +## Common mistakes + +- Keying local logic on `category_id` instead of `aus_keys` +- Dropping hidden entries from the fallback so it no longer mirrors the live response +- Adding a settings row for a display-only category +- Updating the section registry but not the type-keyed maps, which only fail at compile time From e8675df0ab1cc48c894b53c7561e7736136955ff Mon Sep 17 00:00:00 2001 From: Pedro Brighenti Date: Thu, 1 Oct 2026 18:03:36 +0100 Subject: [PATCH 2/2] docs(platform): sync category skill with Mobile --- .../repos/metamask-mobile.md | 11 +++++++---- .../skills/notifications-category-sync/skill.md | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md b/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md index bb6a955a..3677f99d 100644 --- a/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md +++ b/domains/platform/skills/notifications-category-sync/repos/metamask-mobile.md @@ -9,9 +9,12 @@ parent: notifications-category-sync | Concern | Path | |---------|------| -| Fallback snapshot + `resolveNotificationCategories` (mobile filter, empty → fallback) | `app/util/notifications/categories/notification-categories-api.ts` | +| Fallback snapshot + `resolveNotificationCategories` (mobile filter, empty list → fallback) | `app/util/notifications/categories/notification-categories-api.ts` | +| Category request, loading state, and fetch-settled fallback behavior | `app/util/notifications/hooks/useNotifications.ts`; `app/util/notifications/categories/categories-fetch-settled.ts` | | AUS key → i18n stem (`AUS_KEY_TO_I18N_STEM`) | `app/util/notifications/categories/notification-categories-i18n.ts` | -| Flag gate (`socialAI`) and in-app inbox filtering | `app/util/notifications/categories/notifications-settings-types.ts` | +| Flag gate (`socialAI`) and in-app inbox preference filtering | `app/util/notifications/categories/notifications-settings-types.ts` | +| Notification list filtering and category-scoped mark-as-read | `app/components/Views/Notifications/index.tsx` | +| Inbox behavior tests, including category/All mark-as-read | `app/components/Views/Notifications/index.test.tsx` | | Section registry (`NOTIFICATION_SETTINGS_SECTIONS`, slugs, deeplink resolver) | `app/components/Views/Settings/NotificationsSettings/notificationSettingsSections.ts` | | Settings rows + unsupported-category `Logger.error` | `app/components/Views/Settings/NotificationsSettings/index.tsx` | | Per-section detail maps (`SETTINGS_TYPE_BY_SECTION`, layout) | `app/components/Views/Settings/NotificationsSettings/NotificationSettingsSectionContent.tsx` | @@ -26,7 +29,7 @@ parent: notifications-category-sync curl -s https://notification.api.cx.metamask.io/api/v4/notifications/categories \ | jq -S 'map(.visible_on |= sort | .aus_keys |= sort | .notification_types |= sort)' ``` - Compare with `FALLBACK_NOTIFICATION_CATEGORIES` and update `notification-categories-api.test.ts`. If every new category has empty `aus_keys`, skip to Verify. + Compare with `FALLBACK_NOTIFICATION_CATEGORIES` and update `notification-categories-api.test.ts`. The current resolver also uses the fallback when the category list is empty, not only when a request fails. If every new category has empty `aus_keys`, skip to Verify. 3. **Package**: check the key exists in the installed `@metamask/authenticated-user-storage` `NotificationPreferences` (`package.json`). 4. **Copy**: add the key to `AUS_KEY_TO_I18N_STEM`; add `app_settings.notifications_opts._title` and `_desc` to `locales/languages/en.json` only. Other locale files are updated by the translation process, so leave them untouched. Extend `notification-categories-i18n.test.ts`. 5. **Registry**: add the kebab slug to `NotificationSettingsSectionSlug` and an entry to `NOTIFICATION_SETTINGS_SECTIONS` (`slug`, `type` = AUS key, `titleKey`, `descriptionKey`, a valid `IconName`, `showStatus`, `requiresSocialLeaderboard` only if flag-gated). Add the key to the maps in `NotificationSettingsSectionContent.tsx`; both are `Record`, so `yarn lint:tsc` flags a miss. Update `notificationSettingsSections.test.ts` and the section content tests. @@ -36,7 +39,7 @@ parent: notifications-category-sync - `featureNotificationsGateConfig.ts` and `notifications.feature_gate..*` locale keys, only if the feature uses the gate - flag gate in `getNotificationsSettingsSectionConfigs` if the category is flag-gated - new `notification_types` needing inbox rendering: `notification-states/` and `TRIGGER_TYPES` from `@metamask/notification-services-controller` -7. **Tests**: `NotificationsSettings/index.test.tsx` and `NotificationsSettings.view.test.tsx`; the unsupported log uses `Logger.error` from `app/util/Logger`. +7. **Tests**: `NotificationsSettings/index.test.tsx` and `NotificationsSettings.view.test.tsx`; the unsupported log uses `Logger.error` from `app/util/Logger`. Cover inbox filtering and category-scoped mark-as-read in `app/components/Views/Notifications/index.test.tsx`. The settings test checks one log on initial render; it does not guarantee deduplication across later category updates. ## Verify diff --git a/domains/platform/skills/notifications-category-sync/skill.md b/domains/platform/skills/notifications-category-sync/skill.md index f8b234a9..bc3dd4da 100644 --- a/domains/platform/skills/notifications-category-sync/skill.md +++ b/domains/platform/skills/notifications-category-sync/skill.md @@ -28,10 +28,10 @@ Each entry: `category_id`, `aus_keys[]`, `notification_types[]`, `visible_on[]`. - Array order is display order. - A client renders only entries whose `visible_on` includes its platform. Empty `visible_on` means hidden on every platform but still present in the manifest. -- `aus_keys` are the toggle pivot. Non-empty with no local section is a client bug: log the error once and hide the row. Empty means display-only (inbox tab and filtering, no settings row, no error). +- `aus_keys` are the toggle pivot. Non-empty with no local section is a client bug: log the error and hide the row. Empty means display-only (inbox tab and filtering, no settings row, no error). - `category_id` is backend-owned and may not match local naming. Resolve local sections and copy through `aus_keys`, never `category_id`. - `notification_types` is advisory; notifications carry a server-resolved `category` (`''` means uncategorized). -- The fallback snapshot mirrors the live response 1:1, hidden entries included, same order. It is used only when the request fails. +- Keep the fallback snapshot aligned with the live response, including hidden entries and backend order. Clients may also use the fallback when the endpoint returns an empty category list. Check the consuming client’s resolver for its exact fallback condition. ## Workflow @@ -45,7 +45,7 @@ Each entry: `category_id`, `aus_keys[]`, `notification_types[]`, `visible_on[]`. 4. **Add presentation for the key**: copy (title and description, in the source locale; other locales follow the repo's translation process), icon, deeplink slug, whether the row shows channel status, and any feature-flag gate. 5. **Register the section** in the local registry and every per-key map the compiler flags (analytics settings type, detail-screen layout). 6. **Mirror per-key touchpoints.** Grep an existing key in all spellings (camelCase, snake_case, kebab-case) and update every per-category list: deeplink docs and tests, API mocks, feature-gate config. -7. **Update the fallback snapshot and tests**: snapshot mirror, settings-row rendering, the unsupported-category log (called once for non-empty `aus_keys` with no section, never for empty), and display-only skipping. +7. **Update the fallback snapshot and tests**: snapshot mirror, settings-row rendering, the unsupported-category log for non-empty `aus_keys` with no section, and display-only skipping. Assert the intended logging frequency separately if the client promises deduplication. 8. **Verify.** Re-run step 1 (no diff left), then the repo's unit tests, type check and lint. Do not guess a multi-key category's copy or icon; ask. Do not bump dependencies silently.