sanook-cli 0.4.0 → 0.5.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 (235) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +394 -51
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +2 -2
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/registry.js +133 -1
  37. package/dist/repomap.js +93 -0
  38. package/dist/search/chunk.js +158 -0
  39. package/dist/search/embed-store.js +187 -0
  40. package/dist/search/engine.js +203 -0
  41. package/dist/search/fuse.js +35 -0
  42. package/dist/search/index-core.js +187 -0
  43. package/dist/search/indexer.js +241 -0
  44. package/dist/search/store.js +77 -0
  45. package/dist/session.js +42 -8
  46. package/dist/skill-install.js +10 -10
  47. package/dist/skills.js +12 -9
  48. package/dist/summarize.js +31 -0
  49. package/dist/tools/bash.js +21 -2
  50. package/dist/tools/diagnostics.js +41 -0
  51. package/dist/tools/edit.js +29 -7
  52. package/dist/tools/index.js +8 -1
  53. package/dist/tools/list.js +7 -2
  54. package/dist/tools/permission.js +90 -9
  55. package/dist/tools/read.js +23 -4
  56. package/dist/tools/remember.js +1 -1
  57. package/dist/tools/sandbox.js +61 -0
  58. package/dist/tools/search.js +105 -4
  59. package/dist/tools/task.js +195 -29
  60. package/dist/tools/timeout.js +35 -0
  61. package/dist/tools/util.js +10 -0
  62. package/dist/tools/write.js +6 -4
  63. package/dist/trust.js +89 -0
  64. package/dist/ui/app.js +218 -27
  65. package/dist/ui/banner.js +4 -9
  66. package/dist/ui/history.js +30 -0
  67. package/dist/ui/mentions.js +44 -0
  68. package/dist/ui/setup.js +6 -5
  69. package/dist/ui/useEditor.js +83 -0
  70. package/dist/update.js +114 -0
  71. package/dist/worktree.js +173 -0
  72. package/package.json +11 -5
  73. package/scripts/postinstall.mjs +33 -0
  74. package/second-brain/.agents/_Index.md +30 -0
  75. package/second-brain/.agents/skills/_Index.md +30 -0
  76. package/second-brain/.agents/workflows/_Index.md +30 -0
  77. package/second-brain/AGENTS.md +4 -4
  78. package/second-brain/Acceptance/_Index.md +30 -0
  79. package/second-brain/Acceptance/golden-case-template.md +39 -0
  80. package/second-brain/Areas/_Index.md +30 -0
  81. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  82. package/second-brain/Bugs/_Index.md +30 -0
  83. package/second-brain/CLAUDE.md +4 -1
  84. package/second-brain/Checklists/_Index.md +30 -0
  85. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  86. package/second-brain/Distillations/_Index.md +30 -0
  87. package/second-brain/Entities/_Index.md +30 -0
  88. package/second-brain/Entities/entity-template.md +33 -0
  89. package/second-brain/Evals/_Index.md +30 -0
  90. package/second-brain/Evals/correction-pairs.md +24 -0
  91. package/second-brain/Evals/failure-taxonomy.md +24 -0
  92. package/second-brain/Evals/golden-set.md +25 -0
  93. package/second-brain/Evals/quality-ledger.md +23 -0
  94. package/second-brain/Evals/self-eval-rubric.md +23 -0
  95. package/second-brain/GEMINI.md +4 -4
  96. package/second-brain/Goals/_Index.md +30 -0
  97. package/second-brain/Handoffs/_Index.md +30 -0
  98. package/second-brain/Home.md +7 -0
  99. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  100. package/second-brain/Intake/_Index.md +30 -0
  101. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  102. package/second-brain/Learning/_Index.md +30 -0
  103. package/second-brain/Playbooks/_Index.md +30 -0
  104. package/second-brain/Playbooks/playbook-template.md +23 -0
  105. package/second-brain/Projects/_Index.md +30 -0
  106. package/second-brain/Prompts/_Index.md +30 -0
  107. package/second-brain/README.md +2 -1
  108. package/second-brain/Research/_Index.md +30 -0
  109. package/second-brain/Retrospectives/_Index.md +30 -0
  110. package/second-brain/Reviews/_Index.md +30 -0
  111. package/second-brain/Runbooks/_Index.md +30 -0
  112. package/second-brain/Runbooks/eval-loop.md +24 -0
  113. package/second-brain/Sessions/_Index.md +30 -0
  114. package/second-brain/Shared/AI-Context-Index.md +20 -0
  115. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  116. package/second-brain/Shared/Archive/_Index.md +30 -0
  117. package/second-brain/Shared/Assets/_Index.md +30 -0
  118. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  119. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  120. package/second-brain/Shared/Coordination/NOW.md +28 -0
  121. package/second-brain/Shared/Coordination/_Index.md +30 -0
  122. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  123. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  124. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  125. package/second-brain/Shared/Coordination/task-board.md +32 -0
  126. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  127. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  128. package/second-brain/Shared/Glossary/_Index.md +30 -0
  129. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  130. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  131. package/second-brain/Shared/Prompting/_Index.md +30 -0
  132. package/second-brain/Shared/Provenance/_Index.md +30 -0
  133. package/second-brain/Shared/Rules/_Index.md +30 -0
  134. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  135. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  136. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  137. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  138. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  139. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  140. package/second-brain/Shared/Scripts/_Index.md +30 -0
  141. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  142. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  143. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  144. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  145. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  146. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  147. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/_Index.md +30 -0
  149. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  150. package/second-brain/Skills/_Index.md +30 -0
  151. package/second-brain/Templates/_Index.md +30 -0
  152. package/second-brain/Templates/bug.md +2 -0
  153. package/second-brain/Templates/handoff.md +2 -0
  154. package/second-brain/Templates/session.md +2 -0
  155. package/second-brain/Tools/_Index.md +30 -0
  156. package/second-brain/Traces/_Index.md +30 -0
  157. package/second-brain/Vault Structure Map.md +33 -1
  158. package/second-brain/copilot/_Index.md +30 -0
  159. package/skills/audit-license-compliance/SKILL.md +117 -0
  160. package/skills/author-codemod/SKILL.md +110 -0
  161. package/skills/build-audit-logging/SKILL.md +112 -0
  162. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  163. package/skills/build-cli-tool/SKILL.md +108 -0
  164. package/skills/build-data-table/SKILL.md +141 -0
  165. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  166. package/skills/build-offline-first-sync/SKILL.md +118 -0
  167. package/skills/build-realtime-channel/SKILL.md +122 -0
  168. package/skills/build-vector-search/SKILL.md +131 -0
  169. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  170. package/skills/configure-bundler-build/SKILL.md +166 -0
  171. package/skills/configure-dns-tls/SKILL.md +142 -0
  172. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  173. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  174. package/skills/contract-testing/SKILL.md +140 -0
  175. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  176. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  177. package/skills/debug-flaky-tests/SKILL.md +128 -0
  178. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  179. package/skills/deliver-webhooks/SKILL.md +116 -0
  180. package/skills/design-api-pagination/SKILL.md +144 -0
  181. package/skills/design-authorization-model/SKILL.md +119 -0
  182. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  183. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  184. package/skills/design-multi-tenancy/SKILL.md +100 -0
  185. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  186. package/skills/design-relational-schema/SKILL.md +129 -0
  187. package/skills/design-search-index-infra/SKILL.md +151 -0
  188. package/skills/design-state-machine/SKILL.md +108 -0
  189. package/skills/design-token-system/SKILL.md +109 -0
  190. package/skills/distributed-locks-leases/SKILL.md +120 -0
  191. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  192. package/skills/feature-flags-rollout/SKILL.md +130 -0
  193. package/skills/file-upload-object-storage/SKILL.md +107 -0
  194. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  195. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  196. package/skills/i18n-localization-setup/SKILL.md +113 -0
  197. package/skills/idempotency-keys/SKILL.md +107 -0
  198. package/skills/implement-push-notifications/SKILL.md +142 -0
  199. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  200. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  201. package/skills/load-stress-test/SKILL.md +129 -0
  202. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  203. package/skills/model-nosql-data/SKILL.md +118 -0
  204. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  205. package/skills/monitor-ml-drift/SKILL.md +109 -0
  206. package/skills/numeric-precision-units/SKILL.md +144 -0
  207. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  208. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  209. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  210. package/skills/payments-billing-integration/SKILL.md +114 -0
  211. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  212. package/skills/plan-strangler-migration/SKILL.md +95 -0
  213. package/skills/property-based-testing/SKILL.md +108 -0
  214. package/skills/publish-package-registry/SKILL.md +130 -0
  215. package/skills/recover-git-state/SKILL.md +119 -0
  216. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  217. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  218. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  219. package/skills/rewrite-git-history/SKILL.md +109 -0
  220. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  221. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  222. package/skills/send-transactional-email/SKILL.md +126 -0
  223. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  224. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  225. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  226. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  227. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  228. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  229. package/skills/structured-output-llm/SKILL.md +86 -0
  230. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  231. package/skills/test-data-factories/SKILL.md +158 -0
  232. package/skills/threat-model-stride/SKILL.md +123 -0
  233. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  234. package/skills/unicode-text-correctness/SKILL.md +109 -0
  235. package/skills/visual-regression-testing/SKILL.md +120 -0
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: scaffold-cross-platform-app
3
+ description: Scaffolds React Native (Expo Router) and Flutter app shells — feature-first folder layout, typed navigation + deep links, client-store wiring (Zustand/Redux Toolkit/Riverpod/Bloc), platform-divergent code and native bridges (Expo config plugin/Flutter platform channel), token-driven theming with dark mode, and env/build-flavor tooling.
4
+ when_to_use: Standing up or restructuring a whole React Native (Expo) or Flutter app — choosing navigation, client state, platform-conditional code, bridging a native module, theming, and build flavors. Distinct from build-native-mobile-ui (SwiftUI/Compose screens, not RN/Flutter), manage-client-server-state (server cache/data fetching), design-token-system (the token pipeline this skill consumes), and ship-mobile-app-store-release (signing + store upload).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the request is about **standing up or reorganizing a whole RN/Flutter app**, not a single screen:
10
+
11
+ - "Set up a new Expo app with tabs + a typed navigation stack and deep links"
12
+ - "Start a Flutter app with go_router and Riverpod, organized by feature"
13
+ - "Pick state management — Redux Toolkit vs Zustand / Bloc vs Riverpod — and wire it"
14
+ - "I need iOS-only and Android-only versions of this code / an adaptive widget"
15
+ - "Bridge a native module / write an Expo config plugin / add a Flutter platform channel"
16
+ - "Apply our design tokens + dark mode across the app shell"
17
+ - "Add dev/staging/prod flavors with separate env, bundle IDs, and icons"
18
+
19
+ NOT this skill:
20
+ - A **native** iOS/Android screen in SwiftUI or Jetpack Compose (not RN/Flutter) → build-native-mobile-ui
21
+ - Building one reusable RN component in an existing tree → build-react-component
22
+ - Server-cache, fetching, optimistic updates, query invalidation → manage-client-server-state (this skill wires *client* state only)
23
+ - Designing the token **architecture/pipeline** (primitive/semantic tiers, Style Dictionary, W3C export) → design-token-system (this skill *consumes* the exported tokens)
24
+ - Pixel-matching a Figma/screenshot for a screen → implement-from-design
25
+ - Tailwind/responsive web layout → style-responsive-tailwind
26
+ - E2E flows on the running app → write-playwright-e2e
27
+ - Code signing, keystores, TestFlight/Play upload, phased rollout → ship-mobile-app-store-release
28
+ - The CI workflow that calls build/sign/upload lanes (EAS/Codemagic/Fastlane in CI) → cicd-pipeline-author
29
+ - Storing signing keys / API secrets safely → secrets-management
30
+
31
+ ## Steps
32
+
33
+ 1. **Pick the framework lane and don't drift mid-project.** Default to **Expo (managed) + Expo Router** for RN, **Flutter stable + go_router** for Dart. Go bare RN only when a dependency needs native build config the managed prebuild can't express.
34
+
35
+ | Need | RN choice | Flutter choice |
36
+ |---|---|---|
37
+ | Standard app, OTA updates, fast start | **Expo managed** + `expo-dev-client` | Flutter stable |
38
+ | Custom native code you control | Expo + **config plugin** (stay managed) | Flutter + plugin/FFI |
39
+ | Native build settings Expo can't model | bare RN (`expo prebuild` then own `ios/`,`android/`) | n/a |
40
+ | Routing | **Expo Router** (file-based, typed) | **go_router** (typed routes) |
41
+ | New project command | `npx create-expo-app@latest -t default` | `flutter create --org com.acme app` |
42
+
43
+ Reject React-Navigation-only (no router) for new apps: Expo Router *is* React Navigation underneath but gives file-based deep linking for free.
44
+
45
+ 2. **Lay out feature-first, not type-first.** Group by domain so a feature is one deletable folder. Avoid the top-level `screens/ components/ reducers/` split — it scatters every feature across the tree.
46
+
47
+ ```
48
+ src/
49
+ app/ # Expo Router routes (file = route). Flutter: lib/routing/
50
+ (tabs)/index.tsx # deep link: myapp:// → /
51
+ (tabs)/profile.tsx
52
+ post/[id].tsx # myapp://post/42
53
+ _layout.tsx # Stack/Tabs + theme provider
54
+ features/
55
+ auth/ { ui/ store.ts api.ts types.ts }
56
+ feed/ { ui/ store.ts api.ts }
57
+ shared/ { ui/ hooks/ theme/ lib/ }
58
+ platform/ # *.ios.tsx / *.android.tsx live next to use site
59
+ ```
60
+ Flutter mirror: `lib/features/<x>/{presentation,application,data,domain}`, `lib/core/theme`, `lib/routing/app_router.dart`.
61
+
62
+ 3. **Make routes typed and deep-linkable from day one.**
63
+ - **Expo Router:** enable typed routes in `app.json` → `"experiments": { "typedRoutes": true }`. Set `scheme` in `app.json` (`"scheme": "myapp"`) so `myapp://post/42` resolves; for universal/app links add `expo-router` `+native-intent` or `associatedDomains`. Nest with `_layout.tsx`: a `(tabs)` group holds `<Tabs>`, a sibling `_layout` holds a `<Stack>` for modals/detail. Navigate with `router.push({ pathname: '/post/[id]', params: { id } })` — params are type-checked.
64
+ - **go_router:** define routes once, use `GoRoute` + `context.goNamed('post', pathParameters: {'id': id})`. Configure `MaterialApp.router(routerConfig: appRouter)`. Deep links work via the platform `<intent-filter>` (Android) / `CFBundleURLTypes` (iOS) — wire `uriPrefix`/`scheme` to match the route table.
65
+
66
+ 4. **Wire client state by app shape — opinionated defaults, no "it depends":**
67
+
68
+ | App | RN | Flutter | Why |
69
+ |---|---|---|---|
70
+ | Small/medium, mostly local UI state | **Zustand** | **Riverpod** | Minimal boilerplate, no provider-tree gymnastics |
71
+ | Large, many devs, time-travel/devtools, strict conventions | **Redux Toolkit** | **Bloc** | Enforced structure, traceable events, predictable reducers |
72
+ | Server data (lists, caches, mutations) | **TanStack Query** | Riverpod `AsyncNotifier` / `dio` | Don't hand-roll cache in the store → manage-client-server-state |
73
+
74
+ Default to **Zustand** (RN) / **Riverpod** (Flutter) unless team size or audit needs push you to RTK/Bloc. **Boundary rule:** keep *server cache* out of the global store; the store holds session, auth, theme, navigation-adjacent UI state. One `store.ts`/notifier per feature; compose at app root, never one god-store.
75
+
76
+ ```ts
77
+ // features/auth/store.ts — Zustand slice, typed, selector-friendly
78
+ export const useAuth = create<AuthState>()((set) => ({
79
+ user: null, token: null,
80
+ signIn: async (c) => { const { user, token } = await api.login(c); set({ user, token }); },
81
+ signOut: () => set({ user: null, token: null }),
82
+ }));
83
+ // read narrowly to avoid re-renders: const user = useAuth(s => s.user)
84
+ ```
85
+
86
+ 5. **Diverge by platform with the cheapest tool that works.** Escalate only as needed:
87
+ - **One value differs:** `Platform.select({ ios: 12, android: 8, default: 8 })` or `Platform.OS === 'ios'`. Flutter: `Theme.of(context).platform == TargetPlatform.iOS` or `defaultTargetPlatform`.
88
+ - **A whole component differs:** split files — `Button.ios.tsx` / `Button.android.tsx`; import `./Button` and Metro resolves per-platform. Flutter: `Platform.isIOS ? CupertinoButton(...) : ElevatedButton(...)`, or conditional imports for web vs native.
89
+ - **Adaptive by design:** Flutter `Switch.adaptive`, `CupertinoIcons` on iOS; RN use a wrapper that picks the native control. Never branch on `Platform.OS` deep inside business logic — isolate divergence at the UI/platform layer.
90
+
91
+ 6. **Bridge native code through the framework's official channel — never patch generated folders by hand.**
92
+ - **Expo config plugin** (stay managed): write a plugin that mutates native config at prebuild, e.g. `withInfoPlist` / `withAndroidManifest`, register in `app.json` `"plugins": ["./plugins/with-foo"]`. For real native APIs use the **Expo Modules API** (`createModule`, Swift/Kotlin) — typed JS interface, no manual bridge boilerplate.
93
+ - **Bare RN:** Turbo/Native Module — declare a TS spec, run Codegen, implement on iOS (Swift/ObjC) + Android (Kotlin/Java).
94
+ - **Flutter platform channel:** `MethodChannel('com.acme/foo')` on Dart side; implement the matching handler in `AppDelegate.swift` and `MainActivity.kt`. Keep the channel name and method strings in one shared constants file so both sides can't drift.
95
+ ```dart
96
+ const _ch = MethodChannel('com.acme/battery');
97
+ Future<int> level() async => await _ch.invokeMethod<int>('getLevel') ?? -1;
98
+ ```
99
+ After any native change run `expo prebuild --clean` (Expo) or `flutter clean` and rebuild — JS/Dart hot reload will NOT pick up native edits.
100
+
101
+ 7. **Consume design tokens at the app shell; theme from them, don't hardcode.** Build the token source/pipeline with design-token-system; *this* step wires its output into RN/Flutter theming. One `theme/tokens.ts` (or `core/theme/tokens.dart`) holds colors/spacing/radii/typography. Build light+dark from the same tokens; resolve via system scheme.
102
+ - **RN:** export a `light`/`dark` theme object keyed off tokens; read `useColorScheme()`; pass to a `ThemeProvider` (or Expo Router's `<ThemeProvider value={scheme === 'dark' ? Dark : Light}>`). Never inline hex in components — pull from theme.
103
+ - **Flutter:** `MaterialApp(theme: lightFromTokens, darkTheme: darkFromTokens, themeMode: ThemeMode.system)`; build `ColorScheme.fromSeed(seedColor: tokens.brand)`; use `CupertinoTheme` where you ship iOS-native chrome. Dark mode = the dark token set + `themeMode`, not ad-hoc `if (isDark)` checks.
104
+
105
+ 8. **Set up tooling once so the app is reproducible:**
106
+ - **Env/flavors:** RN — `app.config.ts` reading `process.env`, build profiles in `eas.json` (`development`/`preview`/`production`), distinct `bundleIdentifier`/`package` per profile. Flutter — `--flavor dev|staging|prod` with `--dart-define-from-file=env/dev.json`, matching Xcode schemes + Android `productFlavors`. **Secrets never in `app.json`/committed `.env`** → secrets-management. **Signing certs, keystores, and store upload** are out of scope → ship-mobile-app-store-release.
107
+ - **Fonts/assets:** RN `expo-font` `useFonts()` (or `expo-asset` preload), gate render on loaded; Flutter declare under `pubspec.yaml` `fonts:`/`assets:`.
108
+ - **Types/lint:** TS `strict: true`, `eslint` + `eslint-config-expo`, `prettier`; Flutter `flutter analyze` + `flutter_lints`. Add a `typecheck` script (`tsc --noEmit`) to CI.
109
+ - **Fast refresh** is on by default — if it stops working, it's almost always a non-component export or a circular import, not the bundler.
110
+
111
+ ## Common Errors
112
+
113
+ - **Type-first folders (`screens/`, `reducers/`, `components/`).** Every feature smears across the tree; deleting a feature touches 6 folders. Group by feature, share only truly shared code in `shared/`.
114
+ - **One global store for everything including server data.** Caching API responses in Zustand/Redux means manual invalidation and stale UI. Put server cache in TanStack Query / Riverpod `AsyncNotifier`; keep the store for session/UI state.
115
+ - **`Platform.OS` checks buried in business logic.** Divergence leaks everywhere and is untestable. Isolate it at the UI/platform layer via `.ios`/`.android` files or `Platform.select`.
116
+ - **Editing `ios/` or `android/` by hand on a managed Expo app.** The next `prebuild` wipes it. Express native changes as a **config plugin** or Expo Module instead.
117
+ - **Native change with no rebuild.** Hot reload/Fast Refresh only reloads JS/Dart. A new native module or channel needs `expo prebuild --clean` / `flutter clean` + a fresh native build, or you'll debug a phantom "method not found."
118
+ - **Hardcoded hex colors / magic spacing.** Dark mode and rebrands become a find-and-replace. Pull every color/space/radius from the token theme; derive light+dark from one source.
119
+ - **Missing `scheme` / intent-filter, so deep links silently no-op.** Set `scheme` in `app.json` (RN) and the Android `<intent-filter>` + iOS `CFBundleURLTypes` (Flutter) to match the route table, or `myapp://post/42` opens the app to the home screen.
120
+ - **Mismatched platform-channel/method names across Dart↔native.** A typo yields a silent `MissingPluginException` at runtime. Keep channel + method strings in one shared constant referenced by both sides.
121
+ - **Same `bundleIdentifier`/`applicationId` across flavors.** Dev and prod overwrite each other on-device and can't coexist. Give each flavor a distinct id + icon + display name.
122
+ - **Untyped navigation params.** `router.push('/post/' + id)` loses type-checking and breaks on refactor. Enable typed routes (Expo) / named go_router routes and pass params as objects.
123
+
124
+ ## Verify
125
+
126
+ Run on **both** an iOS simulator and an Android emulator/device — a single-platform pass proves nothing cross-platform.
127
+
128
+ 1. **Boots clean both OSes:** `npx expo run:ios` and `npx expo run:android` (or `flutter run -d ios` / `-d android`) start with **no red box / no exception**, app reaches the first screen.
129
+ 2. **Typed navigation + deep links:** a wrong route param fails `tsc --noEmit`/`flutter analyze`. `xcrun simctl openurl booted myapp://post/42` and `adb shell am start -a android.intent.action.VIEW -d "myapp://post/42"` both open the correct detail screen with the right id.
130
+ 3. **State wiring:** an action mutates the store and exactly the subscribed components re-render (verify with a render log/devtools); unrelated screens do not. Server data lives in the query cache, not the store.
131
+ 4. **Platform divergence resolves:** the `.ios`/`.android` (or adaptive) variant renders the native-looking control on each OS — confirm by screenshot, not assumption.
132
+ 5. **Native bridge round-trips:** call the module/channel method on both platforms and get a real value back (not `-1`/`MissingPluginException`); confirm a rebuild was done after the native edit.
133
+ 6. **Theming + dark mode:** toggle system appearance on each OS → colors/typography flip via tokens, no hardcoded color survives; no contrast regressions.
134
+ 7. **Flavors:** build `dev` and `prod` → distinct bundle id + icon + name, each reading its own env, no committed secret in the bundle.
135
+ 8. **Lint/types green:** `tsc --noEmit` + `eslint .` (or `flutter analyze`) pass with zero errors.
136
+
137
+ Done = the app builds and runs on iOS *and* Android, deep links and typed nav resolve on both, state/theming/native-bridge round-trip correctly per platform, and lint + typecheck are green.
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: schema-evolution-compatibility
3
+ description: Evolves shared data contracts (events, API payloads, DB columns, protobuf/avro) without breaking live consumers — additive-only changes with optional+default fields, NEVER remove/rename/repurpose a field or reuse a protobuf tag / avro position (reserve them with `reserved`/aliases instead), backward vs forward vs full compatibility chosen per producer/consumer upgrade order, expand-then-contract (dual-write/dual-read) migrations for renames and type changes, and a schema registry (Confluent/Buf) wired into CI to mechanically reject incompatible diffs before merge. Tolerant reader, unknown-field preservation, and explicit versioning when a true break is unavoidable.
4
+ when_to_use: Changing a schema that something else already reads or writes — adding/removing/renaming a field on a Kafka event, API JSON payload, protobuf/avro/JSON-Schema, or a DB column other services depend on; deciding if a change is safe to deploy and in what order; or wiring registry compat checks into CI. Distinct from design-protobuf-grpc-service (designs the IDL/RPCs from scratch; this evolves an existing one safely) and db-migration-safety (runs the ALTER without locking/downtime; this decides whether the column change breaks readers at all).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when a contract that *another* process already produces or consumes is changing and you must not break it mid-deploy:
10
+
11
+ - "Add a field to this Kafka event / API response — will old consumers still parse it?"
12
+ - "Rename / remove / change the type of a field that other services read"
13
+ - "Which compatibility mode (backward/forward/full) for this Avro subject?"
14
+ - "We reused a protobuf field number and a consumer is reading garbage"
15
+ - "Deploy producers or consumers first? what's the safe order?"
16
+ - "Wire `buf breaking` / Confluent compat checks into CI so bad diffs get blocked"
17
+ - "Migrate a column/field rename with zero downtime across services"
18
+
19
+ NOT this skill:
20
+ - Designing the proto/gRPC service, message shapes, and RPCs from scratch → design-protobuf-grpc-service (this skill *evolves* an IDL that already has live readers)
21
+ - Running the `ALTER TABLE` itself without locks/downtime (lock-free index, batched backfill, `NOT VALID` constraints) → db-migration-safety (it makes the DDL safe; this skill decides if the column change breaks consumers)
22
+ - Designing the relational schema / normalization / keys → design-relational-schema
23
+ - The REST/GraphQL field-type and nullability contract for one endpoint → rest-graphql-contract
24
+ - API versioning policy, deprecation headers, pagination contracts → api-design-review / design-api-pagination
25
+ - Validating one payload against a schema at the edge (request validation) → build-form-validation / validate-data-quality
26
+ - Verifying producer and consumer agree via recorded pacts → contract-testing (it tests the agreement; this skill governs how the schema may change)
27
+ - Big phased rewrite/cutover of a whole system → plan-strangler-migration
28
+
29
+ ## Steps
30
+
31
+ 1. **Pick the compatibility mode from your upgrade order — it's the whole game.** Compatibility is asymmetric and defined by *who reads data written under the other schema*:
32
+
33
+ | Mode | Guarantees | Allowed change | Upgrade FIRST |
34
+ |---|---|---|---|
35
+ | **BACKWARD** | new consumer reads data from old + new producers | **add** optional field (w/ default), **delete** optional field | **consumers** |
36
+ | **FORWARD** | old consumer reads data from new producer | **add** optional field, **delete** field that had a default | **producers** |
37
+ | **FULL** | both directions | **only** add/remove **optional fields with defaults** | either |
38
+ | **\*_TRANSITIVE** | same, but vs **all** prior versions not just the last | — | — |
39
+
40
+ Default to **BACKWARD** for events/topics (Confluent's default — consumers lag and replay history, so the new reader must handle old records). Use **FORWARD** when producers ship ahead of consumers. Use **FULL_TRANSITIVE** for long-lived event logs you replay from the beginning. The rule of thumb: **add a field → forward-safe; remove a field → backward-safe; do both safely → only optional+default**.
41
+
42
+ 2. **Additive-only is the safe default. Every new field is optional with a default — never required.** A new *required* field breaks every old producer (forward) and every old record (backward) instantly. Concretely:
43
+ - **JSON / JSON-Schema:** add the key, do NOT add it to `required`, give consumers a default. Keep `additionalProperties` permissive (or `unevaluatedProperties` in 2020-12) so old readers tolerate fields they don't know.
44
+ - **protobuf (proto3):** every field is already optional; new scalar fields default to `0`/`""`/`false`. Just append with a **fresh field number**. Use `optional` (proto3 explicit presence) when you must distinguish "unset" from "zero".
45
+ - **Avro:** a new field **must** carry a `"default"`, or it's neither backward- nor forward-compatible — `{"name":"x","type":["null","string"],"default":null}`. This is the #1 Avro footgun.
46
+
47
+ 3. **NEVER remove, rename, or repurpose a field in place — and NEVER reuse a tag/number/position.** Renaming = remove + add to every consumer; changing a field's *meaning* while keeping its name/number is the worst break because it passes schema checks but silently corrupts data. Reuse of an identifier makes old payloads decode into the wrong field. Reserve instead:
48
+ - **protobuf** — when you drop field `7` (name `email`), reserve both so the number and name can never be re-added:
49
+ ```proto
50
+ message User {
51
+ reserved 7, 9 to 11; // numbers
52
+ reserved "email", "legacy_id"; // names
53
+ string username = 3;
54
+ }
55
+ ```
56
+ - **Avro** — never reuse a removed field's name; to *rename* keep the old name reachable via `"aliases": ["old_name"]` so readers using the old schema still resolve it.
57
+ - **JSON** — treat a removed key as permanently retired; never recycle a key name for a different type/meaning.
58
+
59
+ A type change (e.g. `int32 → string`, `string → enum`) is **not** additive even if the name stays — it's a remove-and-add. Wire-compatible widenings exist in proto (`int32`/`int64`/`uint32`/`bool` are interchangeable on the wire; `sint*`/`fixed*` are **not**) but treat them as breaking unless you've verified the exact pair.
60
+
61
+ 4. **For a true rename or type change, run expand → migrate → contract (dual-write/dual-read).** You cannot atomically change a field across N independently-deployed services. Phase it:
62
+
63
+ | Phase | Producer | Consumer | DB column |
64
+ |---|---|---|---|
65
+ | **1 Expand** | write BOTH `old` + `new` | still reads `old` | add `new` col, backfill, dual-write trigger |
66
+ | **2 Migrate** | writes both | switch reads to `new` (fallback to `old`) | — |
67
+ | **3 Contract** | stop writing `old`; reserve it | reads `new` only | drop `old` col (after grace + replay window) |
68
+
69
+ Each phase is independently deployable and rollback-safe. The grace window between expand and contract must exceed your **longest consumer lag + replay/retention window** (e.g. Kafka topic retention) so no in-flight or replayed record still needs the old field. The DB column drop is where db-migration-safety takes over.
70
+
71
+ 5. **Deploy in the order the compatibility mode dictates — getting this backwards is the classic outage.**
72
+ - **BACKWARD** change (added/removed optional): deploy **consumers first**, then producers. New consumers can read both shapes; once all consumers handle the new shape, flip producers.
73
+ - **FORWARD** change: deploy **producers first** — old consumers tolerate the new field (they ignore unknowns), then upgrade consumers to use it.
74
+ - **FULL**: either order, but still roll out gradually and watch dead-letter/parse-error metrics during the canary.
75
+ - Never deploy producer and consumer in lockstep assuming atomicity — there is always a window where mixed versions run.
76
+
77
+ 6. **Run a schema registry with mechanical compatibility checks, and gate CI on them.** Humans miss breaks; the registry doesn't.
78
+ - **Confluent Schema Registry** (Avro/Protobuf/JSON-Schema over Kafka): set per-subject mode and test the candidate before publishing — `curl -X PUT .../config/<subject> -d '{"compatibility":"BACKWARD_TRANSITIVE"}'`, then `POST .../compatibility/subjects/<subject>/versions/latest` returns `{"is_compatible": true|false}`. The Maven/Gradle `schema-registry:test-compatibility` goal does this in CI.
79
+ - **protobuf** → **`buf breaking --against '.git#branch=main'`** in CI; rules `FIELD_NO_DELETE` (forces `reserved`), `FIELD_SAME_TYPE`, `RESERVED_*` catch exactly the breaks above. Pair with `buf lint`.
80
+ - **Avro** standalone → `java -jar avro-tools` or the `avro-compatibility` checker; gate the PR.
81
+ - **JSON-Schema** → `json-schema-diff` / `oasdiff` (for OpenAPI) flag breaking changes.
82
+
83
+ Make the check **fail the build**, not warn. The registry's `compatibility` setting per subject is the contract; CI is the enforcement.
84
+
85
+ 7. **Write consumers as tolerant readers — ignore unknown fields, never hard-fail on them.** Forward compatibility depends on the *reader's* behavior as much as the schema:
86
+ - JSON: don't use a strict/closed deserializer that throws on unknown keys. Jackson → `@JsonIgnoreProperties(ignoreUnknown = true)` / `FAIL_ON_UNKNOWN_PROPERTIES=false`; Go `encoding/json` ignores unknowns by default (avoid `DisallowUnknownFields`); Pydantic → `model_config = ConfigDict(extra="ignore")` (NOT `"forbid"`).
87
+ - **Preserve, don't drop, unknown fields** on a read-modify-write path, or a round-trip through an old service silently deletes data a newer one added. protobuf keeps unknown fields by default; for JSON, capture them (`@JsonAnySetter`, `additionalProperties` map) and re-emit. This is the subtle one — a "harmless" old service in the middle of a pipeline strips new fields.
88
+ - Always provide a default when a field is absent; don't assume presence.
89
+
90
+ 8. **When a break is genuinely unavoidable, version explicitly — don't mutate in place.** Some changes (splitting one field into two, restructuring nesting, semantic redefinition) can't be made compatible. Then:
91
+ - **Events:** new schema = **new subject / new topic** (`orders.v2`) or an explicit `schema_version` field; run v1 and v2 in parallel; migrate consumers; retire v1 after the replay window. Never silently change `v1`'s meaning.
92
+ - **APIs:** new path/header version (`/v2`, `Accept: application/vnd.api.v2+json`); deprecate v1 with a sunset header and timeline.
93
+ Versioning is the escape hatch, not the default — additive evolution avoids a version bump for the 90% case.
94
+
95
+ ## Common Errors
96
+
97
+ - **Adding a required field.** Breaks every old producer and every historical record at once. Fix: optional + default, always.
98
+ - **Avro field with no `default`.** Silently fails both backward and forward compat. Fix: every Avro field added/removed needs an explicit `"default"`.
99
+ - **Reusing a protobuf field number (or Avro position).** Old payloads decode into the wrong field — type-confused garbage that passes schema checks. Fix: `reserved` the number AND the name; only ever append fresh numbers.
100
+ - **Renaming a field in place.** It's a delete + add to every consumer simultaneously. Fix: expand→migrate→contract, or Avro `aliases`.
101
+ - **Repurposing a field's meaning while keeping its name.** Passes all mechanical checks, silently corrupts semantics. Fix: new field; reserve the old one.
102
+ - **Wrong deploy order for the compat mode.** Backward change with producers-first (or forward with consumers-first) → mixed-version outage. Fix: consumers-first for backward, producers-first for forward.
103
+ - **Strict deserializer that throws on unknown fields.** Kills forward compatibility the moment a producer adds a field. Fix: tolerant reader (`ignoreUnknown`, `extra="ignore"`, no `DisallowUnknownFields`).
104
+ - **Dropping unknown fields on read-modify-write.** An older service in the pipeline silently erases data newer services added. Fix: preserve and re-emit unknown fields.
105
+ - **Treating a type widening as free.** `int32→string` or `string→enum` is a break even with the same name; not all proto widenings are wire-safe. Fix: verify the exact pair or run expand→contract.
106
+ - **No registry / CI gate.** Relying on review to catch breaks. Fix: `buf breaking` / Confluent compat check that **fails the build**.
107
+ - **Checking only against the latest version, not all.** A change compatible with v3 but not v1 breaks replay. Fix: `*_TRANSITIVE` mode for replayable logs.
108
+ - **Contracting before the replay/retention window passes.** Dropping the old field while replayable records still reference it. Fix: grace window > longest consumer lag + topic retention.
109
+
110
+ ## Verify
111
+
112
+ 1. **Mechanical compat check passes in CI:** `buf breaking` / Confluent `is_compatible:true` / Avro checker runs on the PR diff and **fails the build** on an incompatible change — proven by intentionally introducing a remove/rename and watching CI go red.
113
+ 2. **Old-schema read of new data, and vice versa:** serialize a record with the new schema, deserialize with the old (forward); serialize with old, read with new (backward) — both succeed, defaults fill absent fields. This is the literal compatibility definition; test it, don't assume it.
114
+ 3. **No required field added, every new field has a default:** grep the diff — new fields are optional and defaulted (`"default"` in Avro, not in JSON `required`, appended proto numbers).
115
+ 4. **Removed fields are reserved:** any dropped proto field has its number AND name in `reserved`; any renamed Avro field has `aliases`; no identifier is reused.
116
+ 5. **Tolerant reader confirmed:** feed a consumer a payload with an extra unknown field → it parses and ignores it (no exception); on read-modify-write, the unknown field survives the round-trip.
117
+ 6. **Deploy order documented and rehearsed:** the rollout plan states consumers-first (backward) or producers-first (forward), and a mixed-version canary shows zero parse errors / dead-letters during the window.
118
+ 7. **Rename via expand→contract, not in place:** the migration is staged (dual-write, switch reads, then drop + reserve) and each phase is independently rollback-safe; the old field is dropped only after the replay window.
119
+ 8. **Transitive check for replayable logs:** for an event log replayed from offset 0, compat mode is `*_TRANSITIVE` and a candidate is checked against all prior versions, not just latest.
120
+
121
+ Done = the change is additive (optional + defaulted) or staged through expand→migrate→contract, no field/tag/position is ever removed-without-reserving or repurposed, the compatibility mode matches the deploy order, consumers are tolerant readers that preserve unknowns, and a schema-registry compat check fails CI on any incompatible diff — all proven by the old↔new round-trip and the red-CI test in checks 1–2.
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: send-transactional-email
3
+ description: Ships reliable transactional email (password resets, receipts, verification, alerts) where the hard part is deliverability, not the API call — authenticate the From domain with SPF/DKIM/DMARC alignment, send through a provider (SES/Postmark/SendGrid/Resend/Mailgun) instead of a cold self-hosted MTA, isolate transactional from marketing streams, build inlined-CSS multipart emails, send idempotently via a job runner, and process bounce/complaint webhooks into a suppression list so mail actually lands in the inbox.
4
+ when_to_use: Sending or fixing delivery of transactional email — auth/verification/reset/receipt mail landing in spam, domain authentication (SPF/DKIM/DMARC), bounce/complaint handling, suppression lists, or rendering. Distinct from implement-push-notifications (the mobile/web PUSH channel, a different transport entirely) and message-queue-jobs (the async job system that ENQUEUES the send and owns retry/DLQ — this skill owns the email-specific deliverability, content, and feedback loop).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when the work is **getting a transactional email into the inbox and reacting to what bounces** — domain auth, provider routing, content, and the feedback loop:
10
+
11
+ - "Password-reset / verification emails are landing in spam (or vanishing) — fix deliverability"
12
+ - "Set up SPF / DKIM / DMARC so our From domain authenticates and aligns"
13
+ - "Pick and wire a provider (SES, Postmark, SendGrid, Resend, Mailgun) for receipts/alerts"
14
+ - "Our marketing blasts are tanking password-reset delivery — separate the streams"
15
+ - "Process bounce + complaint webhooks and stop re-sending to dead addresses"
16
+ - "Build the email so it renders right in Outlook/Gmail/dark mode with a plain-text fallback"
17
+ - "A retry double-sent the receipt / verification email — make sends idempotent"
18
+
19
+ NOT this skill:
20
+ - The async job/queue that **enqueues** the send, owns retry-with-backoff, DLQ, poison-message handling → message-queue-jobs (this skill is what runs *inside* that job)
21
+ - The idempotency-key store/dedup primitive that makes the enqueue+send exactly-once → idempotency-keys
22
+ - Mobile/web **PUSH** notifications (APNs/FCM/Web Push) — a different transport, not email → implement-push-notifications
23
+ - The raw DNS record mechanics (TTL, zone editing, how a TXT/CNAME is published) → configure-dns-tls (this skill tells you *which* records; that skill publishes them)
24
+ - Tracking-pixel/open-tracking consent, unsubscribe-data handling, PII retention/erasure → map-privacy-data-gdpr
25
+ - Throttling how many emails one user can trigger → rate-limiting
26
+ - Marketing campaigns, newsletters, drip sequences, segmentation → (out of scope — a different sending stream entirely; see step 3)
27
+
28
+ This skill owns **domain authentication, provider/stream choice, email content, idempotent sending, and feedback processing**. It hands the actual job-running to message-queue-jobs.
29
+
30
+ ## Steps
31
+
32
+ 1. **Authenticate the sending domain — this is the gate, not optional.** Gmail/Yahoo require SPF + DKIM + DMARC on bulk and increasingly on all mail; without alignment you go to spam or get rejected. Publish all three on the From domain (records owned by configure-dns-tls; *values* below). Use a dedicated subdomain like `mail.example.com` / `txn.example.com` so reputation is scoped.
33
+
34
+ | Record | Where | Value (shape) | Purpose |
35
+ |---|---|---|---|
36
+ | **SPF** | `TXT` at sending domain | `v=spf1 include:amazonses.com ~all` (one TXT, ≤10 DNS lookups, `~all` not `-all` until verified) | authorizes the provider's IPs in `Return-Path` |
37
+ | **DKIM** | provider-given `CNAME`s (SES, Resend) or `TXT` (`<sel>._domainkey`) | provider publishes the public key; mail is signed `d=example.com` | cryptographic signature, survives forwarding |
38
+ | **DMARC** | `TXT` at `_dmarc.example.com` | `v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=s; aspf=s` | tells receivers what to do on auth fail + reports |
39
+
40
+ **Alignment is the part people miss:** DMARC passes only if SPF *or* DKIM passes **and** its domain matches the **visible `From:`** domain. `Return-Path: bounces@provider.com` aligning SPF to the provider does **not** align to your From — so DKIM `d=` must equal your From domain. Set a **custom Return-Path / MAIL FROM** subdomain (`bounce.example.com`) at the provider for SPF alignment too. Roll DMARC `p=none` → monitor `rua` reports for 1–4 weeks → `p=quarantine` → `p=reject`. Never start at `reject`; you'll blackhole your own mail.
41
+
42
+ 2. **Send through a reputable provider — do NOT run your own SMTP MTA on cold IPs.** A fresh cloud IP has zero reputation and is often already on a blocklist; running Postfix yourself means you own PTR, warmup, FBL enrollment, and blocklist fights. Use a provider:
43
+
44
+ | Provider | Best for | Notes |
45
+ |---|---|---|
46
+ | **Postmark** | pure transactional, fastest inbox | hard-blocks marketing on transactional streams; great deliverability |
47
+ | **Amazon SES** | volume, cost | cheapest; you do more setup; sandbox until prod access granted |
48
+ | **Resend** | DX-first, modern stacks | React-email native; simple DKIM CNAMEs |
49
+ | **SendGrid / Mailgun** | scale, both streams | bigger surface, more knobs |
50
+
51
+ If you self-host anyway (rare): set **PTR / reverse DNS** so the IP resolves back to your HELO hostname (no PTR ≈ instant spam), enroll in every provider's **FBL**, and warm the IP. For 99% of cases, a provider is the answer.
52
+
53
+ 3. **Separate TRANSACTIONAL from MARKETING — different subdomains, IPs, and streams.** A marketing complaint must **never** be able to poison password-reset delivery. Use `txn.example.com` (or a dedicated transactional stream/IP pool) for resets/receipts/verification, and `news.example.com` (separate IP/stream) for campaigns. Postmark enforces this with separate Streams; SES uses separate **configuration sets** + dedicated IP pools. Mixing them means one bad newsletter tanks your ability to log users in.
54
+
55
+ 4. **Dedicated vs shared IP, and warm up before volume.** Shared IP (provider's pool) is fine and *better* at low/spiky volume — you inherit the pool's warm reputation. Move to a **dedicated IP** only above ~100k/month steady, then **warm it**: ramp send volume gradually so receivers learn the IP is legit.
56
+
57
+ | Day | Max sends/day (rough) |
58
+ |---|---|
59
+ | 1–2 | 50 → 100 |
60
+ | 3–5 | 500 → 1,000 |
61
+ | 6–10 | 5,000 → 20,000 |
62
+ | 11–20 | double daily toward target |
63
+
64
+ Send your **best, most-engaged traffic first** during warmup; complaints early on a cold dedicated IP are very expensive.
65
+
66
+ 5. **Build the email so it actually renders — inline CSS, multipart, dark-mode, accessible.** Email clients (esp. Outlook/Word engine, Gmail) strip `<style>`, ignore flexbox/grid, and need table layout. Use **MJML** (compiles to bulletproof tables) or a templating tool with a **CSS inliner** (`juice`, premailer) — never raw `<div>` flexbox.
67
+ - **Always send `multipart/alternative`** with both `text/plain` AND `text/html`. A missing/empty plain-text part is a strong spam signal and breaks watches/screen readers.
68
+ - **Inline every style** (`style="…"` on elements); media queries in `<head>` for mobile/dark-mode are progressive enhancement only.
69
+ - **Dark mode:** set `<meta name="color-scheme" content="light dark">` and `supported-color-schemes`; don't rely on transparent PNG logos (add a background).
70
+ - **Accessible:** real `alt` on images (many clients block images by default — the email must make sense with images off), sufficient contrast, semantic headings, descriptive link text (not "click here").
71
+ - Put the critical action (reset link, code) in **text**, not baked into an image.
72
+
73
+ 6. **Set From / Reply-To / Return-Path correctly.** `From:` = a real, branded, *authenticated* address on your sending domain (`noreply@txn.example.com` is fine but a monitored `Reply-To` is friendlier). `Reply-To:` → where humans actually reach you (`support@example.com`). **`Return-Path` / envelope MAIL FROM** → the provider's/your bounce-handling address on an SPF-aligned subdomain; this is where bounces go and what SPF checks — **never** your visible From. Mismatched/spoofed From domains fail DMARC.
74
+
75
+ 7. **Make every send idempotent — a retry must not double-send.** The job runner (message-queue-jobs) will retry on transient failure; without a guard, the user gets two receipts. Compute a stable **idempotency key** per logical email (e.g. `sha256(user_id + email_type + event_id)`) and record it transactionally before/with the send. Most providers also accept a request-level idempotency/dedup token — pass it. (The dedup-store primitive is idempotency-keys; this skill defines *what makes an email send unique*.)
76
+
77
+ ```python
78
+ key = sha256(f"{user_id}:password_reset:{reset_request_id}").hexdigest()
79
+ if not claim_idempotency_key(key): # atomic INSERT … ON CONFLICT DO NOTHING
80
+ return # already sent — silently no-op
81
+ provider.send(msg, idempotency_key=key) # provider-level dedup too
82
+ ```
83
+ Enqueue the send as a job rather than sending inline in the request path, so a slow provider or 5xx doesn't fail the user's HTTP request — see message-queue-jobs.
84
+
85
+ 8. **Process bounces + complaints and maintain a suppression list — never re-send to dead/complained addresses.** Wire the provider's **webhooks** (SES→SNS, Postmark/SendGrid/Mailgun event webhooks) and feed a `suppression` table that the send path checks *before* every send. Verify webhook signatures (these are untrusted inbound — see ingest-webhook-secure).
86
+
87
+ | Event | Meaning | Action |
88
+ |---|---|---|
89
+ | **Hard bounce** | address doesn't exist | **suppress permanently**, never retry |
90
+ | **Soft bounce** | mailbox full / temporary | retry a few times, then suppress if persistent |
91
+ | **Complaint (FBL)** | user hit "spam" | **suppress permanently**; investigate — this is reputation poison |
92
+ | **Spam / blocked** | content/IP blocked | pause stream, inspect content/reputation |
93
+
94
+ A single complaint costs far more than a lost email. Re-sending to a hard bounce or complainer destroys sender reputation for *everyone* on the stream.
95
+
96
+ 9. **Honor unsubscribe — `List-Unsubscribe` + One-Click — even on transactional.** Gmail/Yahoo bulk rules require a `List-Unsubscribe` header with **one-click** support; even for transactional mail it's good practice (and required if there's any promotional content). Pure system mail (password reset) can be exempt, but adding the header never hurts.
97
+
98
+ ```
99
+ List-Unsubscribe: <https://example.com/u/abc123>, <mailto:unsub@example.com>
100
+ List-Unsubscribe-Post: List-Unsubscribe=One-Click
101
+ ```
102
+ A POST to the URL must unsubscribe with no further interaction. (Consent/unsubscribe *data* handling → map-privacy-data-gdpr.)
103
+
104
+ 10. **Monitor reputation and stay under the thresholds.** Enroll the domain in **Google Postmaster Tools** and watch your provider's dashboards. Hard limits that get you throttled/blocked: **complaint rate < 0.1%** (Gmail's red line is 0.3%, but treat 0.1% as the ceiling), bounce rate low single digits, no blocklist hits. Set alerting on a spike (observability-instrument). A climbing complaint rate is an early warning before a hard block.
105
+
106
+ 11. **Test in a sandbox — NEVER send to real addresses from staging.** Catch a misconfigured loop emailing 50k real users *before* prod.
107
+ - **Local/CI:** capture all SMTP into **Mailpit** or **MailHog** (a fake inbox); assert subject, both MIME parts, and rendered HTML in tests.
108
+ - **Provider sandbox:** **SES sandbox** only delivers to verified addresses; Postmark has a test API token that accepts-but-doesn't-deliver.
109
+ - **Inbox placement / seed list:** before a big change, send to a seed list (GlockApps/provider tools) to see Gmail/Outlook/Yahoo inbox-vs-spam placement.
110
+ - Gate the real provider behind an env flag so staging can only hit Mailpit/sandbox — never live SMTP.
111
+
112
+ 12. **Mind tracking privacy and don't trust open rates.** Open tracking = a 1×1 pixel; **Apple Mail Privacy Protection (MPP)** pre-fetches it, **inflating opens to near-100%** and making opens worthless for engagement. Tracking pixels are personal-data processing under GDPR — needs a lawful basis and arguably consent (→ map-privacy-data-gdpr). For transactional mail, prefer **no open tracking**; if you wrap links for click tracking, keep redirects fast and on your own domain so they don't trip spam filters or break the link on failure.
113
+
114
+ ## Verify
115
+
116
+ 1. **Auth passes and aligns:** send to `check-auth@verifier.port25.com` or mail-tester.com — SPF `pass`, DKIM `pass` with `d=` your From domain, DMARC `pass` with **alignment**. `dig TXT _dmarc.example.com` shows the policy; `dig TXT <sel>._domainkey.example.com` resolves.
117
+ 2. **DMARC ramped safely:** `rua` aggregate reports show your legit mail passing for 1–4 weeks at `p=none` *before* you move to `quarantine`/`reject`.
118
+ 3. **Streams isolated:** a forced complaint/bounce on the marketing stream does **not** appear in or degrade the transactional stream's reputation/dashboards.
119
+ 4. **Renders everywhere:** the HTML shows correctly in Gmail, Outlook (Word engine), Apple Mail, and dark mode; with images blocked the email is still actionable (alt text, text link); a `text/plain` part exists and is non-empty.
120
+ 5. **Idempotent:** trigger the same logical email twice (or force a job retry) → exactly **one** message is delivered; the second is a no-op.
121
+ 6. **Feedback loop works:** send to a provider seed/simulator bounce + complaint address → webhook fires, the address lands in the **suppression** table, and a subsequent send to it is **skipped before** hitting the provider.
122
+ 7. **Unsubscribe one-click:** a POST to the `List-Unsubscribe` URL unsubscribes with no extra step; Gmail shows the unsubscribe affordance.
123
+ 8. **No real mail from non-prod:** staging/CI sends are captured by Mailpit/MailHog/sandbox and cannot reach a real inbox; a deliberate "send to a real address" from staging is blocked.
124
+ 9. **Reputation green:** Google Postmaster shows domain reputation High/Medium, complaint rate **< 0.1%**, no blocklist entries.
125
+
126
+ Done = the From domain passes SPF/DKIM/DMARC with alignment (DMARC ramped p=none→quarantine→reject on real report data), transactional mail goes through a provider on a stream isolated from marketing, emails render with inlined CSS + a plain-text part, sends are idempotent under retry, bounces/complaints flow into a suppression list that the send path honors, and no staging environment can email a real user.
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: serve-deploy-ml-model
3
+ description: Deploys a trained ML model to production — packaging it with the identical training-time preprocessing, registering a versioned model+code+data triple, serving via batch or online REST/gRPC behind a runtime (BentoML/TorchServe/Triton/ONNX), with autoscaling/warmup and canary/shadow rollout — so served predictions reproducibly match offline scoring.
4
+ when_to_use: Taking a trained model to production to generate predictions (package, register, serve, scale, roll out). Distinct from train-evaluate-ml-model (building/evaluating the model), monitor-ml-drift (post-deployment drift/quality monitoring), and deploy-release (generic application deploys with no model artifact).
5
+ ---
6
+
7
+ ## When to Use
8
+
9
+ Reach for this skill when a working model needs to **serve predictions in production**, not when it's still being built:
10
+
11
+ - "Deploy this model so the app/service can call it"
12
+ - "Stand up a REST/gRPC inference endpoint for `model.pkl`/`model.pt`"
13
+ - "Run nightly batch scoring over the warehouse table"
14
+ - "Roll the new model out behind the old one (shadow/canary) before cutting over"
15
+ - "Our predictions in prod don't match what we got in the notebook" (train/serve skew)
16
+ - "Speed up / scale the inference service" (ONNX export, autoscaling, warmup)
17
+
18
+ NOT this skill:
19
+ - Training, hyperparameter search, offline metrics, choosing the model → train-evaluate-ml-model
20
+ - Watching the *live* model for input drift, label delay, quality decay, alerting → monitor-ml-drift
21
+ - Shipping a normal app/service with no model artifact (web app, API, worker) → deploy-release
22
+ - Percentage ramps, kill switches, sticky bucketing for *any* change → feature-flags-rollout (this skill uses it for the model rollout)
23
+ - Latency/cost tuning of an *LLM* prompt/provider path → optimize-llm-cost-latency
24
+
25
+ ## Steps
26
+
27
+ 1. **Package the model WITH its exact preprocessing — this is the #1 cause of train/serve skew.** The artifact must contain the *same* feature/transform code that produced training inputs, not a reimplementation. Fit transforms on train data, serialize the fitted objects, and apply the identical pipeline at serve time.
28
+
29
+ ```python
30
+ # train.py — ONE fitted pipeline = preprocessing + model, saved as a unit
31
+ from sklearn.pipeline import Pipeline
32
+ from sklearn.compose import ColumnTransformer
33
+ import mlflow, mlflow.sklearn
34
+
35
+ pipe = Pipeline([("prep", ColumnTransformer(...)), ("model", clf)]).fit(X_tr, y_tr)
36
+
37
+ with mlflow.start_run():
38
+ mlflow.sklearn.log_model(
39
+ pipe, "model",
40
+ registered_model_name="churn",
41
+ input_example=X_tr.iloc[:5], # captures schema + dtypes
42
+ signature=mlflow.models.infer_signature(X_tr, pipe.predict(X_tr)),
43
+ pip_requirements="requirements.lock", # pinned, == training env
44
+ )
45
+ ```
46
+ Rules: never re-derive features in the serving codebase; serve the fitted `prep+model` as one object. For deep nets, save the transform graph (e.g. `torchvision`/`torchaudio` transforms or a `tf.function` preprocessing layer) *inside* the exported module so the runtime applies it. Stateful features (counts, embeddings, aggregates) computed from a feature store at train time must be read from the **same** store online — recomputing them in app code drifts.
47
+
48
+ 2. **Register and pin model + code + data together.** A model version is meaningless without the code and data snapshot that produced it. Push to a registry (MLflow Model Registry, SageMaker, Vertex, or a tagged OCI artifact) and record, in the run/version metadata: git SHA, training-data version/hash (DVC/Delta/snapshot id), and the locked dependency file. Use registry **stages** (`Staging` → `Production`) or aliases; deploy by *immutable version*, never "latest".
49
+
50
+ 3. **Pick the serving pattern by latency need — decide, don't hedge.**
51
+
52
+ | Pattern | Use when | Interface | Default runtime |
53
+ |---|---|---|---|
54
+ | **Batch / offline** | No realtime need; score a table/file on a schedule | Job writes predictions to warehouse/S3 | Spark / Ray / a plain container in cron/Airflow |
55
+ | **Online (sync)** | A user request blocks on the prediction; p99 budget < ~200 ms | **REST** (simple, debuggable) default; **gRPC** when p99 < 20 ms or high QPS | BentoML / TorchServe / Triton |
56
+ | **Streaming** | React to an event flow (clicks, transactions) continuously | Consume Kafka/Kinesis → predict → emit | Flink / Faust / a Ray Serve consumer |
57
+
58
+ Defaults: **batch unless something blocks on the result** — it's cheaper, simpler, and trivially reproducible. For online, start with **REST + JSON** and only move to gRPC/protobuf when a measured latency budget forces it. Do not build an online endpoint for a nightly report.
59
+
60
+ 4. **Choose the runtime; export to ONNX/TensorRT only when you need the speed.** Server defaults: **BentoML** (Python-first, easy custom logic, batching) for most teams; **Triton** for multi-framework, GPU, dynamic batching at scale; **TorchServe** for pure PyTorch shops. Convert to **ONNX Runtime** (CPU) or **TensorRT** (GPU) when profiling shows the framework runtime is the bottleneck — and **re-verify outputs match** the original within tolerance (atol≈1e-4) before trusting it; quantization/op-set changes silently alter predictions.
61
+
62
+ ```python
63
+ # bento service.py — load a PINNED model version (never "latest"), server-side batching
64
+ import bentoml
65
+ from bentoml.io import JSON
66
+ runner = bentoml.mlflow.get("churn:prod").to_runner() # alias -> immutable version; never churn:latest
67
+ svc = bentoml.Service("churn", runners=[runner])
68
+
69
+ @svc.api(input=JSON(), output=JSON()) # set batchable=True + max_batch_size on the runner config for throughput
70
+ async def predict(rows: list[dict]) -> list[dict]:
71
+ return await runner.predict.async_run(rows)
72
+ ```
73
+
74
+ 5. **Add warmup, resource limits, and autoscaling — in that order.** Cold models cause p99 spikes: run a synthetic prediction at startup (load weights, JIT/CUDA-warm, fill caches) and gate the readiness probe on it so traffic only arrives warm. Set CPU/memory/GPU **requests and limits** from a load test (see load-stress-test), not by guessing. Autoscale on the right signal — **request concurrency / queue depth / GPU util**, not CPU% for GPU models — with `minReplicas ≥ 2` (no cold-start on scale-from-zero for latency-critical paths) and a scale-down stabilization window so it doesn't flap. Pin threads (`OMP_NUM_THREADS`) to avoid oversubscription under the container limit.
75
+
76
+ 6. **Roll out shadow → canary against the current model; keep an instant rollback.** Never hard-cut. **Shadow** first: mirror live traffic to the new version, log its predictions, serve the old model's response to users — compares behavior on real traffic at zero user risk. Then **canary**: route 1% → 10% → 50% → 100% by sticky hashed bucketing, watching guardrail metrics (latency, error rate, and prediction distribution vs the incumbent); auto-halt and revert on breach. Drive the ramp/kill switch with feature-flags-rollout. Rollback = repoint the alias/route to the previous **registered version** (still deployed) — must be one command, seconds, no rebuild.
77
+
78
+ 7. **Lock inference reproducibility end to end.** Serve from the **locked** requirements captured at registration (same library versions, same op-set), pin the base image by digest, set seeds where any stochasticity exists, and freeze the feature-store read path. The contract: the same input row produces a bit-identical (or within-tolerance) prediction in the notebook, the batch job, and the online endpoint.
79
+
80
+ ## Common Errors
81
+
82
+ - **Reimplementing preprocessing in the serving code.** The serving normalizer/encoder/tokenizer drifts from the training one → skew. Serialize and serve the *fitted* pipeline as one artifact; never rewrite the transforms.
83
+ - **Fitting a transform at serve time** (e.g. `StandardScaler().fit(request_batch)`, or imputing with the request's own mean). Must use stats fitted on **training** data, frozen in the artifact.
84
+ - **Deploying "latest"/an unpinned stage.** A retrain silently swaps the model under prod. Deploy an immutable version id; promote via alias (`churn:prod`), not `churn:latest`.
85
+ - **Env mismatch between train and serve.** Different numpy/sklearn/torch/CUDA or ONNX op-set changes outputs. Serve from the exact locked requirements; pin the image by digest.
86
+ - **ONNX/TensorRT export assumed equivalent.** Quantization, fused ops, or op-set bumps shift predictions. Always diff converted vs original outputs on a fixed sample before shipping.
87
+ - **No warmup → readiness flaps.** First requests hit an unloaded/un-JIT'd model and time out; the cold pod is added to the pool before it can serve. Warm at startup and gate readiness on it.
88
+ - **Online endpoint for a batch problem.** Standing up a low-latency REST service to score a table on a schedule wastes cost and adds failure modes. Use a batch job.
89
+ - **Hard cutover with no shadow/canary.** A skew or perf regression hits 100% of traffic instantly. Shadow, then ramp, with auto-rollback.
90
+ - **Single replica / scale-to-zero on a latency path.** Any restart or scale event becomes a user-visible cold start. Keep `minReplicas ≥ 2`.
91
+ - **Autoscaling GPU models on CPU%.** CPU sits low while the GPU saturates → it never scales and latency explodes. Scale on concurrency/queue depth/GPU util.
92
+ - **Stateful features recomputed in app code.** Online aggregates/counts computed differently from the training feature store drift per request. Read from the same store.
93
+ - **No rollback artifact.** The previous version was torn down, so "revert" means a rebuild. Keep the prior registered version deployed and one alias-flip away.
94
+
95
+ ## Verify
96
+
97
+ 1. **Parity (the skew gate):** Take a **fixed** holdout sample, score it three ways — training notebook, the batch job, and the online endpoint — and assert predictions match within tolerance (exact for classification labels; `atol≤1e-4` for probabilities/regression). Any mismatch blocks the deploy. This is the single most important check.
98
+ 2. **ONNX/quantized parity:** If exported, diff converted-runtime outputs vs the original framework on the same sample within tolerance.
99
+ 3. **Schema/contract:** Send a malformed/missing-field request → a clean 4xx, not a 500 or a silently wrong prediction. The logged input signature matches the registered one.
100
+ 4. **Latency/throughput:** Under the target arrival rate (load-stress-test), p95/p99 and sustained QPS meet the documented SLO **with warmup applied** — measure warm, not cold.
101
+ 5. **Warmup/readiness:** A freshly started replica reports ready only after a successful synthetic prediction; first real request is not a cold spike.
102
+ 6. **Autoscaling:** Drive load past the per-replica knee → replicas scale up on the chosen signal and back down after the stabilization window; `minReplicas` is honored at idle.
103
+ 7. **Shadow:** New version receives mirrored traffic and logs predictions while users still get the incumbent's response; their distributions are comparable before any canary.
104
+ 8. **Rollback:** Flip the alias to the previous version and confirm traffic serves the old model within seconds, no rebuild.
105
+ 9. **Reproducibility pin:** The deployed image digest, model version, training-data hash, and git SHA are all recorded together and resolvable from the running service.
106
+
107
+ Done = served predictions match offline scoring on the fixed sample within tolerance, latency/throughput meet the SLO warm, shadow/canary ran with guardrails, and a one-command rollback to the prior registered version is proven.