diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 86d082d..27b3c5d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ "url": "https://github.com/wshobson" }, "metadata": { - "description": "Production-ready workflow orchestration with 65 focused plugins, 91 specialized agents, and 45 tools - optimized for granular installation and minimal token usage", + "description": "Production-ready workflow orchestration with 67 focused plugins, 99 specialized agents, and 107 skills - optimized for granular installation and minimal token usage", "version": "1.3.0" }, "plugins": [ @@ -129,14 +129,19 @@ "./agents/backend-architect.md", "./agents/graphql-architect.md", "./agents/tdd-orchestrator.md", - "./agents/temporal-python-pro.md" + "./agents/temporal-python-pro.md", + "./agents/event-sourcing-architect.md" ], "skills": [ "./skills/api-design-principles", "./skills/architecture-patterns", "./skills/microservices-patterns", "./skills/workflow-orchestration-patterns", - "./skills/temporal-python-testing" + "./skills/temporal-python-testing", + "./skills/event-store-design", + "./skills/projection-patterns", + "./skills/saga-orchestration", + "./skills/cqrs-implementation" ] }, { @@ -166,6 +171,12 @@ "agents": [ "./agents/frontend-developer.md", "./agents/mobile-developer.md" + ], + "skills": [ + "./skills/react-state-management", + "./skills/nextjs-app-router-patterns", + "./skills/react-native-architecture", + "./skills/tailwind-design-system" ] }, { @@ -431,13 +442,18 @@ ], "agents": [ "./agents/ai-engineer.md", - "./agents/prompt-engineer.md" + "./agents/prompt-engineer.md", + "./agents/vector-database-engineer.md" ], "skills": [ "./skills/langchain-architecture", "./skills/llm-evaluation", "./skills/prompt-engineering-patterns", - "./skills/rag-implementation" + "./skills/rag-implementation", + "./skills/embedding-strategies", + "./skills/similarity-search-patterns", + "./skills/vector-index-tuning", + "./skills/hybrid-search-implementation" ] }, { @@ -558,6 +574,12 @@ "agents": [ "./agents/data-engineer.md", "./agents/backend-architect.md" + ], + "skills": [ + "./skills/dbt-transformation-patterns", + "./skills/airflow-dag-patterns", + "./skills/spark-optimization", + "./skills/data-quality-frameworks" ] }, { @@ -587,6 +609,11 @@ "agents": [ "./agents/incident-responder.md", "./agents/devops-troubleshooter.md" + ], + "skills": [ + "./skills/incident-runbook-templates", + "./skills/postmortem-writing", + "./skills/on-call-handoff-patterns" ] }, { @@ -805,13 +832,18 @@ "./agents/hybrid-cloud-architect.md", "./agents/terraform-specialist.md", "./agents/network-engineer.md", - "./agents/deployment-engineer.md" + "./agents/deployment-engineer.md", + "./agents/service-mesh-expert.md" ], "skills": [ "./skills/cost-optimization", "./skills/hybrid-cloud-networking", "./skills/multi-cloud-architecture", - "./skills/terraform-module-library" + "./skills/terraform-module-library", + "./skills/istio-traffic-management", + "./skills/linkerd-patterns", + "./skills/mtls-configuration", + "./skills/service-mesh-observability" ] }, { @@ -1123,10 +1155,15 @@ "./commands/security-dependencies.md" ], "agents": [ - "./agents/security-auditor.md" + "./agents/security-auditor.md", + "./agents/threat-modeling-expert.md" ], "skills": [ - "./skills/sast-configuration" + "./skills/sast-configuration", + "./skills/stride-analysis-patterns", + "./skills/attack-tree-construction", + "./skills/security-requirement-extraction", + "./skills/threat-mitigation-mapping" ] }, { @@ -1417,6 +1454,11 @@ "./agents/mermaid-expert.md", "./agents/tutorial-engineer.md", "./agents/reference-builder.md" + ], + "skills": [ + "./skills/openapi-spec-generation", + "./skills/architecture-decision-records", + "./skills/changelog-automation" ] }, { @@ -1512,6 +1554,10 @@ "commands": [], "agents": [ "./agents/business-analyst.md" + ], + "skills": [ + "./skills/kpi-dashboard-design", + "./skills/data-storytelling" ] }, { @@ -1541,6 +1587,10 @@ "agents": [ "./agents/hr-pro.md", "./agents/legal-advisor.md" + ], + "skills": [ + "./skills/gdpr-data-handling", + "./skills/employment-contract-templates" ] }, { @@ -1655,6 +1705,10 @@ "agents": [ "./agents/quant-analyst.md", "./agents/risk-manager.md" + ], + "skills": [ + "./skills/backtesting-frameworks", + "./skills/risk-metrics-calculation" ] }, { @@ -1717,6 +1771,10 @@ "agents": [ "./agents/unity-developer.md", "./agents/minecraft-bukkit-pro.md" + ], + "skills": [ + "./skills/unity-ecs-patterns", + "./skills/godot-gdscript-patterns" ] }, { @@ -1745,6 +1803,10 @@ ], "agents": [ "./agents/ui-visual-validator.md" + ], + "skills": [ + "./skills/wcag-audit-patterns", + "./skills/screen-reader-testing" ] }, { @@ -1849,6 +1911,11 @@ "./agents/golang-pro.md", "./agents/c-pro.md", "./agents/cpp-pro.md" + ], + "skills": [ + "./skills/rust-async-patterns", + "./skills/go-concurrency-patterns", + "./skills/memory-safety-patterns" ] }, { @@ -2048,7 +2115,9 @@ "category": "development", "strict": false, "commands": [], - "agents": [], + "agents": [ + "./agents/monorepo-architect.md" + ], "skills": [ "./skills/git-advanced-workflows", "./skills/sql-optimization-patterns", @@ -2057,7 +2126,11 @@ "./skills/e2e-testing-patterns", "./skills/auth-implementation-patterns", "./skills/debugging-strategies", - "./skills/monorepo-management" + "./skills/monorepo-management", + "./skills/nx-workspace-patterns", + "./skills/turborepo-caching", + "./skills/bazel-build-optimization", + "./skills/monorepo-dependency-management" ] } ] diff --git a/README.md b/README.md index 36b437c..8476327 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,26 @@ [![Run in Smithery](https://smithery.ai/badge/skills/wshobson)](https://smithery.ai/skills?ns=wshobson&utm_source=github&utm_medium=badge) -> **🎯 Agent Skills Enabled** — 47 specialized skills extend Claude's capabilities across plugins with progressive disclosure +> **🎯 Agent Skills Enabled** — 107 specialized skills extend Claude's capabilities across plugins with progressive disclosure -A comprehensive production-ready system combining **91 specialized AI agents**, **15 multi-agent workflow orchestrators**, **47 agent skills**, and **45 development tools** organized into **65 focused, single-purpose plugins** for [Claude Code](https://docs.claude.com/en/docs/claude-code/overview). +A comprehensive production-ready system combining **99 specialized AI agents**, **15 multi-agent workflow orchestrators**, **107 agent skills**, and **71 development tools** organized into **67 focused, single-purpose plugins** for [Claude Code](https://docs.claude.com/en/docs/claude-code/overview). ## Overview This unified repository provides everything needed for intelligent automation and multi-agent orchestration across modern software development: -- **65 Focused Plugins** - Granular, single-purpose plugins optimized for minimal token usage and composability -- **91 Specialized Agents** - Domain experts with deep knowledge across architecture, languages, infrastructure, quality, data/AI, documentation, business operations, and SEO -- **47 Agent Skills** - Modular knowledge packages with progressive disclosure for specialized expertise +- **67 Focused Plugins** - Granular, single-purpose plugins optimized for minimal token usage and composability +- **99 Specialized Agents** - Domain experts with deep knowledge across architecture, languages, infrastructure, quality, data/AI, documentation, business operations, and SEO +- **107 Agent Skills** - Modular knowledge packages with progressive disclosure for specialized expertise - **15 Workflow Orchestrators** - Multi-agent coordination systems for complex operations like full-stack development, security hardening, ML pipelines, and incident response -- **45 Development Tools** - Optimized utilities including project scaffolding, security scanning, test automation, and infrastructure setup +- **71 Development Tools** - Optimized utilities including project scaffolding, security scanning, test automation, and infrastructure setup ### Key Features -- **Granular Plugin Architecture**: 65 focused plugins optimized for minimal token usage -- **Comprehensive Tooling**: 45 development tools including test generation, scaffolding, and security scanning +- **Granular Plugin Architecture**: 67 focused plugins optimized for minimal token usage +- **Comprehensive Tooling**: 71 development tools including test generation, scaffolding, and security scanning - **100% Agent Coverage**: All plugins include specialized agents -- **Agent Skills**: 47 specialized skills following for progressive disclosure and token efficiency +- **Agent Skills**: 107 specialized skills following for progressive disclosure and token efficiency - **Clear Organization**: 23 categories with 1-6 plugins each for easy discovery - **Efficient Design**: Average 3.4 components per plugin (follows Anthropic's 2-8 pattern) @@ -49,7 +49,7 @@ Add this marketplace to Claude Code: /plugin marketplace add wshobson/agents ``` -This makes all 65 plugins available for installation, but **does not load any agents or tools** into your context. +This makes all 67 plugins available for installation, but **does not load any agents or tools** into your context. ### Step 2: Install Plugins @@ -85,9 +85,9 @@ Each installed plugin loads **only its specific agents, commands, and skills** i ### Core Guides -- **[Plugin Reference](docs/plugins.md)** - Complete catalog of all 65 plugins -- **[Agent Reference](docs/agents.md)** - All 91 agents organized by category -- **[Agent Skills](docs/agent-skills.md)** - 47 specialized skills with progressive disclosure +- **[Plugin Reference](docs/plugins.md)** - Complete catalog of all 67 plugins +- **[Agent Reference](docs/agents.md)** - All 99 agents organized by category +- **[Agent Skills](docs/agent-skills.md)** - 107 specialized skills with progressive disclosure - **[Usage Guide](docs/usage.md)** - Commands, workflows, and best practices - **[Architecture](docs/architecture.md)** - Design principles and patterns @@ -101,7 +101,7 @@ Each installed plugin loads **only its specific agents, commands, and skills** i ## What's New -### Agent Skills (47 skills across 14 plugins) +### Agent Skills (107 skills across 18 plugins) Specialized knowledge packages following Anthropic's progressive disclosure architecture: @@ -205,7 +205,7 @@ Uses kubernetes-architect agent with 4 specialized skills for production-grade c ## Plugin Categories -**23 categories, 65 plugins:** +**23 categories, 67 plugins:** - 🎨 **Development** (4) - debugging, backend, frontend, multi-platform - 📚 **Documentation** (3) - code docs, API specs, diagrams, C4 architecture @@ -237,7 +237,7 @@ Uses kubernetes-architect agent with 4 specialized skills for production-grade c - **Single responsibility** - Each plugin does one thing well - **Minimal token usage** - Average 3.4 components per plugin - **Composable** - Mix and match for complex workflows -- **100% coverage** - All 91 agents accessible across plugins +- **100% coverage** - All 99 agents accessible across plugins ### Progressive Disclosure (Skills) @@ -251,7 +251,7 @@ Three-tier architecture for token efficiency: ``` claude-agents/ ├── .claude-plugin/ -│ └── marketplace.json # 65 plugins +│ └── marketplace.json # 67 plugins ├── plugins/ │ ├── python-development/ │ │ ├── agents/ # 3 Python experts @@ -261,7 +261,7 @@ claude-agents/ │ │ ├── agents/ # K8s architect │ │ ├── commands/ # Deployment tools │ │ └── skills/ # 4 K8s skills -│ └── ... (63 more plugins) +│ └── ... (65 more plugins) ├── docs/ # Comprehensive documentation └── README.md # This file ``` diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 0ea146c..f41b343 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -1,6 +1,6 @@ # Agent Skills -Agent Skills are modular packages that extend Claude's capabilities with specialized domain knowledge, following Anthropic's [Agent Skills Specification](https://github.com/anthropics/skills/blob/main/agent_skills_spec.md). This plugin ecosystem includes **57 specialized skills** across 15 plugins, enabling progressive disclosure and efficient token usage. +Agent Skills are modular packages that extend Claude's capabilities with specialized domain knowledge, following Anthropic's [Agent Skills Specification](https://github.com/anthropics/skills/blob/main/agent_skills_spec.md). This plugin ecosystem includes **107 specialized skills** across 18 plugins, enabling progressive disclosure and efficient token usage. ## Overview @@ -187,7 +187,7 @@ fastapi-templates skill → Supplies production-ready templates ## Specification Compliance -All 55 skills follow the [Agent Skills Specification](https://github.com/anthropics/skills/blob/main/agent_skills_spec.md): +All 107 skills follow the [Agent Skills Specification](https://github.com/anthropics/skills/blob/main/agent_skills_spec.md): - ✓ Required `name` field (hyphen-case) - ✓ Required `description` field with "Use when" clause diff --git a/docs/agents.md b/docs/agents.md index 539cae0..4e5b5e0 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -1,6 +1,6 @@ # Agent Reference -Complete reference for all **91 specialized AI agents** organized by category with model assignments. +Complete reference for all **99 specialized AI agents** organized by category with model assignments. ## Agent Categories diff --git a/docs/architecture.md b/docs/architecture.md index 04d3942..3015734 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ This marketplace follows industry best practices with a focus on granularity, co ### Plugin Distribution -- **65 focused plugins** optimized for specific use cases +- **67 focused plugins** optimized for specific use cases - **23 clear categories** with 1-6 plugins each for easy discovery - Organized by domain: - **Development**: 4 plugins (debugging, backend, frontend, multi-platform) @@ -48,17 +48,17 @@ This marketplace follows industry best practices with a focus on granularity, co ### Component Breakdown -**91 Specialized Agents** +**99 Specialized Agents** - Domain experts with deep knowledge - Organized across architecture, languages, infrastructure, quality, data/AI, documentation, business, and SEO -- Model-optimized (48 Haiku, 100 Sonnet) for performance and cost +- Model-optimized with three-tier strategy (Opus, Sonnet, Haiku) for performance and cost **15 Workflow Orchestrators** - Multi-agent coordination systems - Complex operations like full-stack development, security hardening, ML pipelines, incident response - Pre-configured agent workflows -**44 Development Tools** +**71 Development Tools** - Optimized utilities including: - Project scaffolding (Python, TypeScript, Rust) - Security scanning (SAST, dependency audit, XSS) @@ -66,10 +66,10 @@ This marketplace follows industry best practices with a focus on granularity, co - Component scaffolding (React, React Native) - Infrastructure setup (Terraform, Kubernetes) -**47 Agent Skills** +**107 Agent Skills** - Modular knowledge packages - Progressive disclosure architecture -- Domain-specific expertise across 14 plugins +- Domain-specific expertise across 18 plugins - Spec-compliant (Anthropic Agent Skills Specification) ## Repository Structure @@ -77,7 +77,7 @@ This marketplace follows industry best practices with a focus on granularity, co ``` claude-agents/ ├── .claude-plugin/ -│ └── marketplace.json # Marketplace catalog (65 plugins) +│ └── marketplace.json # Marketplace catalog (67 plugins) ├── plugins/ # Isolated plugin directories │ ├── python-development/ │ │ ├── agents/ # Python language agents @@ -120,7 +120,7 @@ claude-agents/ │ │ │ └── c4-context.md │ │ └── commands/ │ │ └── c4-architecture.md -│ └── ... (60 more isolated plugins) +│ └── ... (62 more isolated plugins) ├── docs/ # Documentation │ ├── agent-skills.md # Agent Skills guide │ ├── agents.md # Agent reference @@ -191,7 +191,7 @@ description: What the skill does. Use when [trigger]. # Required: < 1024 chars - **Composability**: Mix and match skills across workflows - **Maintainability**: Isolated updates don't affect other skills -See [Agent Skills](./agent-skills.md) for complete details on the 47 skills. +See [Agent Skills](./agent-skills.md) for complete details on the 107 skills. ## Model Configuration Strategy @@ -201,8 +201,9 @@ The system uses Claude Opus and Sonnet models strategically: | Model | Count | Use Case | |-------|-------|----------| -| Haiku | 48 agents | Fast execution, deterministic tasks | -| Sonnet | 100 agents | Complex reasoning, architecture decisions | +| Opus | 42 agents | Critical architecture, security, code review | +| Sonnet | 39 agents | Complex tasks, support with intelligence | +| Haiku | 18 agents | Fast operational tasks | ### Selection Criteria @@ -255,7 +256,7 @@ code-reviewer (Sonnet) validates architecture ### Component Coverage - **100% agent coverage** - all plugins include at least one agent -- **100% component availability** - all 91 agents accessible across plugins +- **100% component availability** - all 99 agents accessible across plugins - **Efficient distribution** - 3.4 components per plugin average ### Discoverability @@ -383,5 +384,5 @@ Feature Development Workflow: - [Agent Skills](./agent-skills.md) - Modular knowledge packages - [Agent Reference](./agents.md) - Complete agent catalog -- [Plugin Reference](./plugins.md) - All 65 plugins +- [Plugin Reference](./plugins.md) - All 67 plugins - [Usage Guide](./usage.md) - Commands and workflows diff --git a/docs/plugins.md b/docs/plugins.md index 4720795..956e7b0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,6 +1,6 @@ # Complete Plugin Reference -Browse all **65 focused, single-purpose plugins** organized by category. +Browse all **67 focused, single-purpose plugins** organized by category. ## Quick Start - Essential Plugins @@ -329,7 +329,7 @@ plugins/python-development/ /plugin marketplace add wshobson/agents ``` -This makes all 65 plugins available for installation, but **does not load any agents or tools** into your context. +This makes all 67 plugins available for installation, but **does not load any agents or tools** into your context. ### Step 2: Install Specific Plugins @@ -369,7 +369,7 @@ Each installed plugin loads **only its specific agents and commands** into Claud ## See Also -- [Agent Skills](./agent-skills.md) - 47 specialized skills across plugins +- [Agent Skills](./agent-skills.md) - 107 specialized skills across plugins - [Agent Reference](./agents.md) - Complete agent catalog - [Usage Guide](./usage.md) - Commands and workflows - [Architecture](./architecture.md) - Design principles diff --git a/docs/usage.md b/docs/usage.md index f3bf49e..ef7cc26 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -386,11 +386,11 @@ User: "Implement Kubernetes deployment with Helm" → Result: Production-grade K8s manifests with Helm charts ``` -See [Agent Skills](./agent-skills.md) for details on the 47 specialized skills. +See [Agent Skills](./agent-skills.md) for details on the 107 specialized skills. ## See Also - [Agent Skills](./agent-skills.md) - Specialized knowledge packages - [Agent Reference](./agents.md) - Complete agent catalog -- [Plugin Reference](./plugins.md) - All 65 plugins +- [Plugin Reference](./plugins.md) - All 67 plugins - [Architecture](./architecture.md) - Design principles diff --git a/plugins/accessibility-compliance/skills/screen-reader-testing/SKILL.md b/plugins/accessibility-compliance/skills/screen-reader-testing/SKILL.md new file mode 100644 index 0000000..d40aa37 --- /dev/null +++ b/plugins/accessibility-compliance/skills/screen-reader-testing/SKILL.md @@ -0,0 +1,533 @@ +--- +name: screen-reader-testing +description: Test web applications with screen readers including VoiceOver, NVDA, and JAWS. Use when validating screen reader compatibility, debugging accessibility issues, or ensuring assistive technology support. +--- + +# Screen Reader Testing + +Practical guide to testing web applications with screen readers for comprehensive accessibility validation. + +## When to Use This Skill + +- Validating screen reader compatibility +- Testing ARIA implementations +- Debugging assistive technology issues +- Verifying form accessibility +- Testing dynamic content announcements +- Ensuring navigation accessibility + +## Core Concepts + +### 1. Major Screen Readers + +| Screen Reader | Platform | Browser | Usage | +|---------------|----------|---------|-------| +| **VoiceOver** | macOS/iOS | Safari | ~15% | +| **NVDA** | Windows | Firefox/Chrome | ~31% | +| **JAWS** | Windows | Chrome/IE | ~40% | +| **TalkBack** | Android | Chrome | ~10% | +| **Narrator** | Windows | Edge | ~4% | + +### 2. Testing Priority + +``` +Minimum Coverage: +1. NVDA + Firefox (Windows) +2. VoiceOver + Safari (macOS) +3. VoiceOver + Safari (iOS) + +Comprehensive Coverage: ++ JAWS + Chrome (Windows) ++ TalkBack + Chrome (Android) ++ Narrator + Edge (Windows) +``` + +### 3. Screen Reader Modes + +| Mode | Purpose | When Used | +|------|---------|-----------| +| **Browse/Virtual** | Read content | Default reading | +| **Focus/Forms** | Interact with controls | Filling forms | +| **Application** | Custom widgets | ARIA applications | + +## VoiceOver (macOS) + +### Setup + +``` +Enable: System Preferences → Accessibility → VoiceOver +Toggle: Cmd + F5 +Quick Toggle: Triple-press Touch ID +``` + +### Essential Commands + +``` +Navigation: +VO = Ctrl + Option (VoiceOver modifier) + +VO + Right Arrow Next element +VO + Left Arrow Previous element +VO + Shift + Down Enter group +VO + Shift + Up Exit group + +Reading: +VO + A Read all from cursor +Ctrl Stop speaking +VO + B Read current paragraph + +Interaction: +VO + Space Activate element +VO + Shift + M Open menu +Tab Next focusable element +Shift + Tab Previous focusable element + +Rotor (VO + U): +Navigate by: Headings, Links, Forms, Landmarks +Left/Right Arrow Change rotor category +Up/Down Arrow Navigate within category +Enter Go to item + +Web Specific: +VO + Cmd + H Next heading +VO + Cmd + J Next form control +VO + Cmd + L Next link +VO + Cmd + T Next table +``` + +### Testing Checklist + +```markdown +## VoiceOver Testing Checklist + +### Page Load +- [ ] Page title announced +- [ ] Main landmark found +- [ ] Skip link works + +### Navigation +- [ ] All headings discoverable via rotor +- [ ] Heading levels logical (H1 → H2 → H3) +- [ ] Landmarks properly labeled +- [ ] Skip links functional + +### Links & Buttons +- [ ] Link purpose clear +- [ ] Button actions described +- [ ] New window/tab announced + +### Forms +- [ ] All labels read with inputs +- [ ] Required fields announced +- [ ] Error messages read +- [ ] Instructions available +- [ ] Focus moves to errors + +### Dynamic Content +- [ ] Alerts announced immediately +- [ ] Loading states communicated +- [ ] Content updates announced +- [ ] Modals trap focus correctly + +### Tables +- [ ] Headers associated with cells +- [ ] Table navigation works +- [ ] Complex tables have captions +``` + +### Common Issues & Fixes + +```html + + + + + + + +
New results loaded
+ + +
New results loaded
+ + + +Invalid email + + + +Invalid email +``` + +## NVDA (Windows) + +### Setup + +``` +Download: nvaccess.org +Start: Ctrl + Alt + N +Stop: Insert + Q +``` + +### Essential Commands + +``` +Navigation: +Insert = NVDA modifier + +Down Arrow Next line +Up Arrow Previous line +Tab Next focusable +Shift + Tab Previous focusable + +Reading: +NVDA + Down Arrow Say all +Ctrl Stop speech +NVDA + Up Arrow Current line + +Headings: +H Next heading +Shift + H Previous heading +1-6 Heading level 1-6 + +Forms: +F Next form field +B Next button +E Next edit field +X Next checkbox +C Next combo box + +Links: +K Next link +U Next unvisited link +V Next visited link + +Landmarks: +D Next landmark +Shift + D Previous landmark + +Tables: +T Next table +Ctrl + Alt + Arrows Navigate cells + +Elements List (NVDA + F7): +Shows all links, headings, form fields, landmarks +``` + +### Browse vs Focus Mode + +``` +NVDA automatically switches modes: +- Browse Mode: Arrow keys navigate content +- Focus Mode: Arrow keys control interactive elements + +Manual switch: NVDA + Space + +Watch for: +- "Browse mode" announcement when navigating +- "Focus mode" when entering form fields +- Application role forces forms mode +``` + +### Testing Script + +```markdown +## NVDA Test Script + +### Initial Load +1. Navigate to page +2. Let page finish loading +3. Press Insert + Down to read all +4. Note: Page title, main content identified? + +### Landmark Navigation +1. Press D repeatedly +2. Check: All main areas reachable? +3. Check: Landmarks properly labeled? + +### Heading Navigation +1. Press Insert + F7 → Headings +2. Check: Logical heading structure? +3. Press H to navigate headings +4. Check: All sections discoverable? + +### Form Testing +1. Press F to find first form field +2. Check: Label read? +3. Fill in invalid data +4. Submit form +5. Check: Errors announced? +6. Check: Focus moved to error? + +### Interactive Elements +1. Tab through all interactive elements +2. Check: Each announces role and state +3. Activate buttons with Enter/Space +4. Check: Result announced? + +### Dynamic Content +1. Trigger content update +2. Check: Change announced? +3. Open modal +4. Check: Focus trapped? +5. Close modal +6. Check: Focus returns? +``` + +## JAWS (Windows) + +### Essential Commands + +``` +Start: Desktop shortcut or Ctrl + Alt + J +Virtual Cursor: Auto-enabled in browsers + +Navigation: +Arrow keys Navigate content +Tab Next focusable +Insert + Down Read all +Ctrl Stop speech + +Quick Keys: +H Next heading +T Next table +F Next form field +B Next button +G Next graphic +L Next list +; Next landmark + +Forms Mode: +Enter Enter forms mode +Numpad + Exit forms mode +F5 List form fields + +Lists: +Insert + F7 Link list +Insert + F6 Heading list +Insert + F5 Form field list + +Tables: +Ctrl + Alt + Arrows Table navigation +``` + +## TalkBack (Android) + +### Setup + +``` +Enable: Settings → Accessibility → TalkBack +Toggle: Hold both volume buttons 3 seconds +``` + +### Gestures + +``` +Explore: Drag finger across screen +Next: Swipe right +Previous: Swipe left +Activate: Double tap +Scroll: Two finger swipe + +Reading Controls (swipe up then right): +- Headings +- Links +- Controls +- Characters +- Words +- Lines +- Paragraphs +``` + +## Common Test Scenarios + +### 1. Modal Dialog + +```html + +
+

Confirm Delete

+

This action cannot be undone.

+ + +
+``` + +```javascript +// Focus management +function openModal(modal) { + // Store last focused element + lastFocus = document.activeElement; + + // Move focus to modal + modal.querySelector('h2').focus(); + + // Trap focus + modal.addEventListener('keydown', trapFocus); +} + +function closeModal(modal) { + // Return focus + lastFocus.focus(); +} + +function trapFocus(e) { + if (e.key === 'Tab') { + const focusable = modal.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (e.shiftKey && document.activeElement === first) { + last.focus(); + e.preventDefault(); + } else if (!e.shiftKey && document.activeElement === last) { + first.focus(); + e.preventDefault(); + } + } + + if (e.key === 'Escape') { + closeModal(modal); + } +} +``` + +### 2. Live Regions + +```html + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+``` + +### 3. Tab Interface + +```html +
+ + +
+ +
+ Product description content... +
+ + +``` + +```javascript +// Tab keyboard navigation +tablist.addEventListener('keydown', (e) => { + const tabs = [...tablist.querySelectorAll('[role="tab"]')]; + const index = tabs.indexOf(document.activeElement); + + let newIndex; + switch (e.key) { + case 'ArrowRight': + newIndex = (index + 1) % tabs.length; + break; + case 'ArrowLeft': + newIndex = (index - 1 + tabs.length) % tabs.length; + break; + case 'Home': + newIndex = 0; + break; + case 'End': + newIndex = tabs.length - 1; + break; + default: + return; + } + + tabs[newIndex].focus(); + activateTab(tabs[newIndex]); + e.preventDefault(); +}); +``` + +## Debugging Tips + +```javascript +// Log what screen reader sees +function logAccessibleName(element) { + const computed = window.getComputedStyle(element); + console.log({ + role: element.getAttribute('role') || element.tagName, + name: element.getAttribute('aria-label') || + element.getAttribute('aria-labelledby') || + element.textContent, + state: { + expanded: element.getAttribute('aria-expanded'), + selected: element.getAttribute('aria-selected'), + checked: element.getAttribute('aria-checked'), + disabled: element.disabled + }, + visible: computed.display !== 'none' && computed.visibility !== 'hidden' + }); +} +``` + +## Best Practices + +### Do's +- **Test with actual screen readers** - Not just simulators +- **Use semantic HTML first** - ARIA is supplemental +- **Test in browse and focus modes** - Different experiences +- **Verify focus management** - Especially for SPAs +- **Test keyboard only first** - Foundation for SR testing + +### Don'ts +- **Don't assume one SR is enough** - Test multiple +- **Don't ignore mobile** - Growing user base +- **Don't test only happy path** - Test error states +- **Don't skip dynamic content** - Most common issues +- **Don't rely on visual testing** - Different experience + +## Resources + +- [VoiceOver User Guide](https://support.apple.com/guide/voiceover/welcome/mac) +- [NVDA User Guide](https://www.nvaccess.org/files/nvda/documentation/userGuide.html) +- [JAWS Documentation](https://support.freedomscientific.com/Products/Blindness/JAWS) +- [WebAIM Screen Reader Survey](https://webaim.org/projects/screenreadersurvey/) diff --git a/plugins/accessibility-compliance/skills/wcag-audit-patterns/SKILL.md b/plugins/accessibility-compliance/skills/wcag-audit-patterns/SKILL.md new file mode 100644 index 0000000..dc77244 --- /dev/null +++ b/plugins/accessibility-compliance/skills/wcag-audit-patterns/SKILL.md @@ -0,0 +1,508 @@ +--- +name: wcag-audit-patterns +description: Conduct WCAG 2.2 accessibility audits with automated testing, manual verification, and remediation guidance. Use when auditing websites for accessibility, fixing WCAG violations, or implementing accessible design patterns. +--- + +# WCAG Audit Patterns + +Comprehensive guide to auditing web content against WCAG 2.2 guidelines with actionable remediation strategies. + +## When to Use This Skill + +- Conducting accessibility audits +- Fixing WCAG violations +- Implementing accessible components +- Preparing for accessibility lawsuits +- Meeting ADA/Section 508 requirements +- Achieving VPAT compliance + +## Core Concepts + +### 1. WCAG Conformance Levels + +| Level | Description | Required For | +|-------|-------------|--------------| +| **A** | Minimum accessibility | Legal baseline | +| **AA** | Standard conformance | Most regulations | +| **AAA** | Enhanced accessibility | Specialized needs | + +### 2. POUR Principles + +``` +Perceivable: Can users perceive the content? +Operable: Can users operate the interface? +Understandable: Can users understand the content? +Robust: Does it work with assistive tech? +``` + +### 3. Common Violations by Impact + +``` +Critical (Blockers): +├── Missing alt text for functional images +├── No keyboard access to interactive elements +├── Missing form labels +└── Auto-playing media without controls + +Serious: +├── Insufficient color contrast +├── Missing skip links +├── Inaccessible custom widgets +└── Missing page titles + +Moderate: +├── Missing language attribute +├── Unclear link text +├── Missing landmarks +└── Improper heading hierarchy +``` + +## Audit Checklist + +### Perceivable (Principle 1) + +```markdown +## 1.1 Text Alternatives + +### 1.1.1 Non-text Content (Level A) +- [ ] All images have alt text +- [ ] Decorative images have alt="" +- [ ] Complex images have long descriptions +- [ ] Icons with meaning have accessible names +- [ ] CAPTCHAs have alternatives + +Check: +```html + +Sales increased 25% from Q1 to Q2 + + + + +decorative line +``` + +## 1.2 Time-based Media + +### 1.2.1 Audio-only and Video-only (Level A) +- [ ] Audio has text transcript +- [ ] Video has audio description or transcript + +### 1.2.2 Captions (Level A) +- [ ] All video has synchronized captions +- [ ] Captions are accurate and complete +- [ ] Speaker identification included + +### 1.2.3 Audio Description (Level A) +- [ ] Video has audio description for visual content + +## 1.3 Adaptable + +### 1.3.1 Info and Relationships (Level A) +- [ ] Headings use proper tags (h1-h6) +- [ ] Lists use ul/ol/dl +- [ ] Tables have headers +- [ ] Form inputs have labels +- [ ] ARIA landmarks present + +Check: +```html + +

Page Title

+

Section

+

Subsection

+

Another Section

+ + + + + + +
NamePrice
+``` + +### 1.3.2 Meaningful Sequence (Level A) +- [ ] Reading order is logical +- [ ] CSS positioning doesn't break order +- [ ] Focus order matches visual order + +### 1.3.3 Sensory Characteristics (Level A) +- [ ] Instructions don't rely on shape/color alone +- [ ] "Click the red button" → "Click Submit (red button)" + +## 1.4 Distinguishable + +### 1.4.1 Use of Color (Level A) +- [ ] Color is not only means of conveying info +- [ ] Links distinguishable without color +- [ ] Error states not color-only + +### 1.4.3 Contrast (Minimum) (Level AA) +- [ ] Text: 4.5:1 contrast ratio +- [ ] Large text (18pt+): 3:1 ratio +- [ ] UI components: 3:1 ratio + +Tools: WebAIM Contrast Checker, axe DevTools + +### 1.4.4 Resize Text (Level AA) +- [ ] Text resizes to 200% without loss +- [ ] No horizontal scrolling at 320px +- [ ] Content reflows properly + +### 1.4.10 Reflow (Level AA) +- [ ] Content reflows at 400% zoom +- [ ] No two-dimensional scrolling +- [ ] All content accessible at 320px width + +### 1.4.11 Non-text Contrast (Level AA) +- [ ] UI components have 3:1 contrast +- [ ] Focus indicators visible +- [ ] Graphical objects distinguishable + +### 1.4.12 Text Spacing (Level AA) +- [ ] No content loss with increased spacing +- [ ] Line height 1.5x font size +- [ ] Paragraph spacing 2x font size +- [ ] Letter spacing 0.12x font size +- [ ] Word spacing 0.16x font size +``` + +### Operable (Principle 2) + +```markdown +## 2.1 Keyboard Accessible + +### 2.1.1 Keyboard (Level A) +- [ ] All functionality keyboard accessible +- [ ] No keyboard traps +- [ ] Tab order is logical +- [ ] Custom widgets are keyboard operable + +Check: +```javascript +// Custom button must be keyboard accessible +
+``` + +### 2.1.2 No Keyboard Trap (Level A) +- [ ] Focus can move away from all components +- [ ] Modal dialogs trap focus correctly +- [ ] Focus returns after modal closes + +## 2.2 Enough Time + +### 2.2.1 Timing Adjustable (Level A) +- [ ] Session timeouts can be extended +- [ ] User warned before timeout +- [ ] Option to disable auto-refresh + +### 2.2.2 Pause, Stop, Hide (Level A) +- [ ] Moving content can be paused +- [ ] Auto-updating content can be paused +- [ ] Animations respect prefers-reduced-motion + +```css +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } +} +``` + +## 2.3 Seizures and Physical Reactions + +### 2.3.1 Three Flashes (Level A) +- [ ] No content flashes more than 3 times/second +- [ ] Flashing area is small (<25% viewport) + +## 2.4 Navigable + +### 2.4.1 Bypass Blocks (Level A) +- [ ] Skip to main content link present +- [ ] Landmark regions defined +- [ ] Proper heading structure + +```html + +
...
+``` + +### 2.4.2 Page Titled (Level A) +- [ ] Unique, descriptive page titles +- [ ] Title reflects page content + +### 2.4.3 Focus Order (Level A) +- [ ] Focus order matches visual order +- [ ] tabindex used correctly + +### 2.4.4 Link Purpose (In Context) (Level A) +- [ ] Links make sense out of context +- [ ] No "click here" or "read more" alone + +```html + +Click here + + +Download Q4 Sales Report (PDF) +``` + +### 2.4.6 Headings and Labels (Level AA) +- [ ] Headings describe content +- [ ] Labels describe purpose + +### 2.4.7 Focus Visible (Level AA) +- [ ] Focus indicator visible on all elements +- [ ] Custom focus styles meet contrast + +```css +:focus { + outline: 3px solid #005fcc; + outline-offset: 2px; +} +``` + +### 2.4.11 Focus Not Obscured (Level AA) - WCAG 2.2 +- [ ] Focused element not fully hidden +- [ ] Sticky headers don't obscure focus +``` + +### Understandable (Principle 3) + +```markdown +## 3.1 Readable + +### 3.1.1 Language of Page (Level A) +- [ ] HTML lang attribute set +- [ ] Language correct for content + +```html + +``` + +### 3.1.2 Language of Parts (Level AA) +- [ ] Language changes marked +```html +

The French word bonjour means hello.

+``` + +## 3.2 Predictable + +### 3.2.1 On Focus (Level A) +- [ ] No context change on focus alone +- [ ] No unexpected popups on focus + +### 3.2.2 On Input (Level A) +- [ ] No automatic form submission +- [ ] User warned before context change + +### 3.2.3 Consistent Navigation (Level AA) +- [ ] Navigation consistent across pages +- [ ] Repeated components same order + +### 3.2.4 Consistent Identification (Level AA) +- [ ] Same functionality = same label +- [ ] Icons used consistently + +## 3.3 Input Assistance + +### 3.3.1 Error Identification (Level A) +- [ ] Errors clearly identified +- [ ] Error message describes problem +- [ ] Error linked to field + +```html + +Please enter valid email +``` + +### 3.3.2 Labels or Instructions (Level A) +- [ ] All inputs have visible labels +- [ ] Required fields indicated +- [ ] Format hints provided + +### 3.3.3 Error Suggestion (Level AA) +- [ ] Errors include correction suggestion +- [ ] Suggestions are specific + +### 3.3.4 Error Prevention (Level AA) +- [ ] Legal/financial forms reversible +- [ ] Data checked before submission +- [ ] User can review before submit +``` + +### Robust (Principle 4) + +```markdown +## 4.1 Compatible + +### 4.1.1 Parsing (Level A) - Obsolete in WCAG 2.2 +- [ ] Valid HTML (good practice) +- [ ] No duplicate IDs +- [ ] Complete start/end tags + +### 4.1.2 Name, Role, Value (Level A) +- [ ] Custom widgets have accessible names +- [ ] ARIA roles correct +- [ ] State changes announced + +```html + +
+
+Accept terms +``` + +### 4.1.3 Status Messages (Level AA) +- [ ] Status updates announced +- [ ] Live regions used correctly + +```html +
+ 3 items added to cart +
+ +
+ Error: Form submission failed +
+``` +``` + +## Automated Testing + +```javascript +// axe-core integration +const axe = require('axe-core'); + +async function runAccessibilityAudit(page) { + await page.addScriptTag({ path: require.resolve('axe-core') }); + + const results = await page.evaluate(async () => { + return await axe.run(document, { + runOnly: { + type: 'tag', + values: ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'] + } + }); + }); + + return { + violations: results.violations, + passes: results.passes, + incomplete: results.incomplete + }; +} + +// Playwright test example +test('should have no accessibility violations', async ({ page }) => { + await page.goto('/'); + const results = await runAccessibilityAudit(page); + + expect(results.violations).toHaveLength(0); +}); +``` + +```bash +# CLI tools +npx @axe-core/cli https://example.com +npx pa11y https://example.com +lighthouse https://example.com --only-categories=accessibility +``` + +## Remediation Patterns + +### Fix: Missing Form Labels + +```html + + + + + + + + + + + +Email + +``` + +### Fix: Insufficient Color Contrast + +```css +/* Before: 2.5:1 contrast */ +.text { color: #767676; } + +/* After: 4.5:1 contrast */ +.text { color: #595959; } + +/* Or add background */ +.text { + color: #767676; + background: #000; +} +``` + +### Fix: Keyboard Navigation + +```javascript +// Make custom element keyboard accessible +class AccessibleDropdown extends HTMLElement { + connectedCallback() { + this.setAttribute('tabindex', '0'); + this.setAttribute('role', 'combobox'); + this.setAttribute('aria-expanded', 'false'); + + this.addEventListener('keydown', (e) => { + switch (e.key) { + case 'Enter': + case ' ': + this.toggle(); + e.preventDefault(); + break; + case 'Escape': + this.close(); + break; + case 'ArrowDown': + this.focusNext(); + e.preventDefault(); + break; + case 'ArrowUp': + this.focusPrevious(); + e.preventDefault(); + break; + } + }); + } +} +``` + +## Best Practices + +### Do's +- **Start early** - Accessibility from design phase +- **Test with real users** - Disabled users provide best feedback +- **Automate what you can** - 30-50% issues detectable +- **Use semantic HTML** - Reduces ARIA needs +- **Document patterns** - Build accessible component library + +### Don'ts +- **Don't rely only on automated testing** - Manual testing required +- **Don't use ARIA as first solution** - Native HTML first +- **Don't hide focus outlines** - Keyboard users need them +- **Don't disable zoom** - Users need to resize +- **Don't use color alone** - Multiple indicators needed + +## Resources + +- [WCAG 2.2 Guidelines](https://www.w3.org/TR/WCAG22/) +- [WebAIM](https://webaim.org/) +- [A11y Project Checklist](https://www.a11yproject.com/checklist/) +- [axe DevTools](https://www.deque.com/axe/) diff --git a/plugins/backend-development/agents/event-sourcing-architect.md b/plugins/backend-development/agents/event-sourcing-architect.md new file mode 100644 index 0000000..444c0e4 --- /dev/null +++ b/plugins/backend-development/agents/event-sourcing-architect.md @@ -0,0 +1,42 @@ +# Event Sourcing Architect + +Expert in event sourcing, CQRS, and event-driven architecture patterns. Masters event store design, projection building, saga orchestration, and eventual consistency patterns. Use PROACTIVELY for event-sourced systems, audit trail requirements, or complex domain modeling with temporal queries. + +## Capabilities + +- Event store design and implementation +- CQRS (Command Query Responsibility Segregation) patterns +- Projection building and read model optimization +- Saga and process manager orchestration +- Event versioning and schema evolution +- Snapshotting strategies for performance +- Eventual consistency handling + +## When to Use + +- Building systems requiring complete audit trails +- Implementing complex business workflows with compensating actions +- Designing systems needing temporal queries ("what was state at time X") +- Separating read and write models for performance +- Building event-driven microservices architectures +- Implementing undo/redo or time-travel debugging + +## Workflow + +1. Identify aggregate boundaries and event streams +2. Design events as immutable facts +3. Implement command handlers and event application +4. Build projections for query requirements +5. Design saga/process managers for cross-aggregate workflows +6. Implement snapshotting for long-lived aggregates +7. Set up event versioning strategy + +## Best Practices + +- Events are facts - never delete or modify them +- Keep events small and focused +- Version events from day one +- Design for eventual consistency +- Use correlation IDs for tracing +- Implement idempotent event handlers +- Plan for projection rebuilding diff --git a/plugins/backend-development/skills/cqrs-implementation/SKILL.md b/plugins/backend-development/skills/cqrs-implementation/SKILL.md new file mode 100644 index 0000000..b704f80 --- /dev/null +++ b/plugins/backend-development/skills/cqrs-implementation/SKILL.md @@ -0,0 +1,552 @@ +--- +name: cqrs-implementation +description: Implement Command Query Responsibility Segregation for scalable architectures. Use when separating read and write models, optimizing query performance, or building event-sourced systems. +--- + +# CQRS Implementation + +Comprehensive guide to implementing CQRS (Command Query Responsibility Segregation) patterns. + +## When to Use This Skill + +- Separating read and write concerns +- Scaling reads independently from writes +- Building event-sourced systems +- Optimizing complex query scenarios +- Different read/write data models needed +- High-performance reporting requirements + +## Core Concepts + +### 1. CQRS Architecture + +``` + ┌─────────────┐ + │ Client │ + └──────┬──────┘ + │ + ┌────────────┴────────────┐ + │ │ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ Commands │ │ Queries │ + │ API │ │ API │ + └──────┬──────┘ └──────┬──────┘ + │ │ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ Command │ │ Query │ + │ Handlers │ │ Handlers │ + └──────┬──────┘ └──────┬──────┘ + │ │ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ Write │─────────►│ Read │ + │ Model │ Events │ Model │ + └─────────────┘ └─────────────┘ +``` + +### 2. Key Components + +| Component | Responsibility | +|-----------|---------------| +| **Command** | Intent to change state | +| **Command Handler** | Validates and executes commands | +| **Event** | Record of state change | +| **Query** | Request for data | +| **Query Handler** | Retrieves data from read model | +| **Projector** | Updates read model from events | + +## Templates + +### Template 1: Command Infrastructure + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TypeVar, Generic, Dict, Any, Type +from datetime import datetime +import uuid + +# Command base +@dataclass +class Command: + command_id: str = None + timestamp: datetime = None + + def __post_init__(self): + self.command_id = self.command_id or str(uuid.uuid4()) + self.timestamp = self.timestamp or datetime.utcnow() + + +# Concrete commands +@dataclass +class CreateOrder(Command): + customer_id: str + items: list + shipping_address: dict + + +@dataclass +class AddOrderItem(Command): + order_id: str + product_id: str + quantity: int + price: float + + +@dataclass +class CancelOrder(Command): + order_id: str + reason: str + + +# Command handler base +T = TypeVar('T', bound=Command) + +class CommandHandler(ABC, Generic[T]): + @abstractmethod + async def handle(self, command: T) -> Any: + pass + + +# Command bus +class CommandBus: + def __init__(self): + self._handlers: Dict[Type[Command], CommandHandler] = {} + + def register(self, command_type: Type[Command], handler: CommandHandler): + self._handlers[command_type] = handler + + async def dispatch(self, command: Command) -> Any: + handler = self._handlers.get(type(command)) + if not handler: + raise ValueError(f"No handler for {type(command).__name__}") + return await handler.handle(command) + + +# Command handler implementation +class CreateOrderHandler(CommandHandler[CreateOrder]): + def __init__(self, order_repository, event_store): + self.order_repository = order_repository + self.event_store = event_store + + async def handle(self, command: CreateOrder) -> str: + # Validate + if not command.items: + raise ValueError("Order must have at least one item") + + # Create aggregate + order = Order.create( + customer_id=command.customer_id, + items=command.items, + shipping_address=command.shipping_address + ) + + # Persist events + await self.event_store.append_events( + stream_id=f"Order-{order.id}", + stream_type="Order", + events=order.uncommitted_events + ) + + return order.id +``` + +### Template 2: Query Infrastructure + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TypeVar, Generic, List, Optional + +# Query base +@dataclass +class Query: + pass + + +# Concrete queries +@dataclass +class GetOrderById(Query): + order_id: str + + +@dataclass +class GetCustomerOrders(Query): + customer_id: str + status: Optional[str] = None + page: int = 1 + page_size: int = 20 + + +@dataclass +class SearchOrders(Query): + query: str + filters: dict = None + sort_by: str = "created_at" + sort_order: str = "desc" + + +# Query result types +@dataclass +class OrderView: + order_id: str + customer_id: str + status: str + total_amount: float + item_count: int + created_at: datetime + shipped_at: Optional[datetime] = None + + +@dataclass +class PaginatedResult(Generic[T]): + items: List[T] + total: int + page: int + page_size: int + + @property + def total_pages(self) -> int: + return (self.total + self.page_size - 1) // self.page_size + + +# Query handler base +T = TypeVar('T', bound=Query) +R = TypeVar('R') + +class QueryHandler(ABC, Generic[T, R]): + @abstractmethod + async def handle(self, query: T) -> R: + pass + + +# Query bus +class QueryBus: + def __init__(self): + self._handlers: Dict[Type[Query], QueryHandler] = {} + + def register(self, query_type: Type[Query], handler: QueryHandler): + self._handlers[query_type] = handler + + async def dispatch(self, query: Query) -> Any: + handler = self._handlers.get(type(query)) + if not handler: + raise ValueError(f"No handler for {type(query).__name__}") + return await handler.handle(query) + + +# Query handler implementation +class GetOrderByIdHandler(QueryHandler[GetOrderById, Optional[OrderView]]): + def __init__(self, read_db): + self.read_db = read_db + + async def handle(self, query: GetOrderById) -> Optional[OrderView]: + async with self.read_db.acquire() as conn: + row = await conn.fetchrow( + """ + SELECT order_id, customer_id, status, total_amount, + item_count, created_at, shipped_at + FROM order_views + WHERE order_id = $1 + """, + query.order_id + ) + if row: + return OrderView(**dict(row)) + return None + + +class GetCustomerOrdersHandler(QueryHandler[GetCustomerOrders, PaginatedResult[OrderView]]): + def __init__(self, read_db): + self.read_db = read_db + + async def handle(self, query: GetCustomerOrders) -> PaginatedResult[OrderView]: + async with self.read_db.acquire() as conn: + # Build query with optional status filter + where_clause = "customer_id = $1" + params = [query.customer_id] + + if query.status: + where_clause += " AND status = $2" + params.append(query.status) + + # Get total count + total = await conn.fetchval( + f"SELECT COUNT(*) FROM order_views WHERE {where_clause}", + *params + ) + + # Get paginated results + offset = (query.page - 1) * query.page_size + rows = await conn.fetch( + f""" + SELECT order_id, customer_id, status, total_amount, + item_count, created_at, shipped_at + FROM order_views + WHERE {where_clause} + ORDER BY created_at DESC + LIMIT ${len(params) + 1} OFFSET ${len(params) + 2} + """, + *params, query.page_size, offset + ) + + return PaginatedResult( + items=[OrderView(**dict(row)) for row in rows], + total=total, + page=query.page, + page_size=query.page_size + ) +``` + +### Template 3: FastAPI CQRS Application + +```python +from fastapi import FastAPI, HTTPException, Depends +from pydantic import BaseModel +from typing import List, Optional + +app = FastAPI() + +# Request/Response models +class CreateOrderRequest(BaseModel): + customer_id: str + items: List[dict] + shipping_address: dict + + +class OrderResponse(BaseModel): + order_id: str + customer_id: str + status: str + total_amount: float + item_count: int + created_at: datetime + + +# Dependency injection +def get_command_bus() -> CommandBus: + return app.state.command_bus + + +def get_query_bus() -> QueryBus: + return app.state.query_bus + + +# Command endpoints (POST, PUT, DELETE) +@app.post("/orders", response_model=dict) +async def create_order( + request: CreateOrderRequest, + command_bus: CommandBus = Depends(get_command_bus) +): + command = CreateOrder( + customer_id=request.customer_id, + items=request.items, + shipping_address=request.shipping_address + ) + order_id = await command_bus.dispatch(command) + return {"order_id": order_id} + + +@app.post("/orders/{order_id}/items") +async def add_item( + order_id: str, + product_id: str, + quantity: int, + price: float, + command_bus: CommandBus = Depends(get_command_bus) +): + command = AddOrderItem( + order_id=order_id, + product_id=product_id, + quantity=quantity, + price=price + ) + await command_bus.dispatch(command) + return {"status": "item_added"} + + +@app.delete("/orders/{order_id}") +async def cancel_order( + order_id: str, + reason: str, + command_bus: CommandBus = Depends(get_command_bus) +): + command = CancelOrder(order_id=order_id, reason=reason) + await command_bus.dispatch(command) + return {"status": "cancelled"} + + +# Query endpoints (GET) +@app.get("/orders/{order_id}", response_model=OrderResponse) +async def get_order( + order_id: str, + query_bus: QueryBus = Depends(get_query_bus) +): + query = GetOrderById(order_id=order_id) + result = await query_bus.dispatch(query) + if not result: + raise HTTPException(status_code=404, detail="Order not found") + return result + + +@app.get("/customers/{customer_id}/orders") +async def get_customer_orders( + customer_id: str, + status: Optional[str] = None, + page: int = 1, + page_size: int = 20, + query_bus: QueryBus = Depends(get_query_bus) +): + query = GetCustomerOrders( + customer_id=customer_id, + status=status, + page=page, + page_size=page_size + ) + return await query_bus.dispatch(query) + + +@app.get("/orders/search") +async def search_orders( + q: str, + sort_by: str = "created_at", + query_bus: QueryBus = Depends(get_query_bus) +): + query = SearchOrders(query=q, sort_by=sort_by) + return await query_bus.dispatch(query) +``` + +### Template 4: Read Model Synchronization + +```python +class ReadModelSynchronizer: + """Keeps read models in sync with events.""" + + def __init__(self, event_store, read_db, projections: List[Projection]): + self.event_store = event_store + self.read_db = read_db + self.projections = {p.name: p for p in projections} + + async def run(self): + """Continuously sync read models.""" + while True: + for name, projection in self.projections.items(): + await self._sync_projection(projection) + await asyncio.sleep(0.1) + + async def _sync_projection(self, projection: Projection): + checkpoint = await self._get_checkpoint(projection.name) + + events = await self.event_store.read_all( + from_position=checkpoint, + limit=100 + ) + + for event in events: + if event.event_type in projection.handles(): + try: + await projection.apply(event) + except Exception as e: + # Log error, possibly retry or skip + logger.error(f"Projection error: {e}") + continue + + await self._save_checkpoint(projection.name, event.global_position) + + async def rebuild_projection(self, projection_name: str): + """Rebuild a projection from scratch.""" + projection = self.projections[projection_name] + + # Clear existing data + await projection.clear() + + # Reset checkpoint + await self._save_checkpoint(projection_name, 0) + + # Rebuild + while True: + checkpoint = await self._get_checkpoint(projection_name) + events = await self.event_store.read_all(checkpoint, 1000) + + if not events: + break + + for event in events: + if event.event_type in projection.handles(): + await projection.apply(event) + + await self._save_checkpoint( + projection_name, + events[-1].global_position + ) +``` + +### Template 5: Eventual Consistency Handling + +```python +class ConsistentQueryHandler: + """Query handler that can wait for consistency.""" + + def __init__(self, read_db, event_store): + self.read_db = read_db + self.event_store = event_store + + async def query_after_command( + self, + query: Query, + expected_version: int, + stream_id: str, + timeout: float = 5.0 + ): + """ + Execute query, ensuring read model is at expected version. + Used for read-your-writes consistency. + """ + start_time = time.time() + + while time.time() - start_time < timeout: + # Check if read model is caught up + projection_version = await self._get_projection_version(stream_id) + + if projection_version >= expected_version: + return await self.execute_query(query) + + # Wait a bit and retry + await asyncio.sleep(0.1) + + # Timeout - return stale data with warning + return { + "data": await self.execute_query(query), + "_warning": "Data may be stale" + } + + async def _get_projection_version(self, stream_id: str) -> int: + """Get the last processed event version for a stream.""" + async with self.read_db.acquire() as conn: + return await conn.fetchval( + "SELECT last_event_version FROM projection_state WHERE stream_id = $1", + stream_id + ) or 0 +``` + +## Best Practices + +### Do's +- **Separate command and query models** - Different needs +- **Use eventual consistency** - Accept propagation delay +- **Validate in command handlers** - Before state change +- **Denormalize read models** - Optimize for queries +- **Version your events** - For schema evolution + +### Don'ts +- **Don't query in commands** - Use only for writes +- **Don't couple read/write schemas** - Independent evolution +- **Don't over-engineer** - Start simple +- **Don't ignore consistency SLAs** - Define acceptable lag + +## Resources + +- [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) +- [Microsoft CQRS Guidance](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) diff --git a/plugins/backend-development/skills/event-store-design/SKILL.md b/plugins/backend-development/skills/event-store-design/SKILL.md new file mode 100644 index 0000000..1302110 --- /dev/null +++ b/plugins/backend-development/skills/event-store-design/SKILL.md @@ -0,0 +1,435 @@ +--- +name: event-store-design +description: Design and implement event stores for event-sourced systems. Use when building event sourcing infrastructure, choosing event store technologies, or implementing event persistence patterns. +--- + +# Event Store Design + +Comprehensive guide to designing event stores for event-sourced applications. + +## When to Use This Skill + +- Designing event sourcing infrastructure +- Choosing between event store technologies +- Implementing custom event stores +- Optimizing event storage and retrieval +- Setting up event store schemas +- Planning for event store scaling + +## Core Concepts + +### 1. Event Store Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Event Store │ +├─────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Stream 1 │ │ Stream 2 │ │ Stream 3 │ │ +│ │ (Aggregate) │ │ (Aggregate) │ │ (Aggregate) │ │ +│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ +│ │ Event 1 │ │ Event 1 │ │ Event 1 │ │ +│ │ Event 2 │ │ Event 2 │ │ Event 2 │ │ +│ │ Event 3 │ │ ... │ │ Event 3 │ │ +│ │ ... │ │ │ │ Event 4 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────┤ +│ Global Position: 1 → 2 → 3 → 4 → 5 → 6 → ... │ +└─────────────────────────────────────────────────────┘ +``` + +### 2. Event Store Requirements + +| Requirement | Description | +|-------------|-------------| +| **Append-only** | Events are immutable, only appends | +| **Ordered** | Per-stream and global ordering | +| **Versioned** | Optimistic concurrency control | +| **Subscriptions** | Real-time event notifications | +| **Idempotent** | Handle duplicate writes safely | + +## Technology Comparison + +| Technology | Best For | Limitations | +|------------|----------|-------------| +| **EventStoreDB** | Pure event sourcing | Single-purpose | +| **PostgreSQL** | Existing Postgres stack | Manual implementation | +| **Kafka** | High-throughput streaming | Not ideal for per-stream queries | +| **DynamoDB** | Serverless, AWS-native | Query limitations | +| **Marten** | .NET ecosystems | .NET specific | + +## Templates + +### Template 1: PostgreSQL Event Store Schema + +```sql +-- Events table +CREATE TABLE events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + stream_id VARCHAR(255) NOT NULL, + stream_type VARCHAR(255) NOT NULL, + event_type VARCHAR(255) NOT NULL, + event_data JSONB NOT NULL, + metadata JSONB DEFAULT '{}', + version BIGINT NOT NULL, + global_position BIGSERIAL, + created_at TIMESTAMPTZ DEFAULT NOW(), + + CONSTRAINT unique_stream_version UNIQUE (stream_id, version) +); + +-- Index for stream queries +CREATE INDEX idx_events_stream_id ON events(stream_id, version); + +-- Index for global subscription +CREATE INDEX idx_events_global_position ON events(global_position); + +-- Index for event type queries +CREATE INDEX idx_events_event_type ON events(event_type); + +-- Index for time-based queries +CREATE INDEX idx_events_created_at ON events(created_at); + +-- Snapshots table +CREATE TABLE snapshots ( + stream_id VARCHAR(255) PRIMARY KEY, + stream_type VARCHAR(255) NOT NULL, + snapshot_data JSONB NOT NULL, + version BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Subscriptions checkpoint table +CREATE TABLE subscription_checkpoints ( + subscription_id VARCHAR(255) PRIMARY KEY, + last_position BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +### Template 2: Python Event Store Implementation + +```python +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Optional, List +from uuid import UUID, uuid4 +import json +import asyncpg + +@dataclass +class Event: + stream_id: str + event_type: str + data: dict + metadata: dict = field(default_factory=dict) + event_id: UUID = field(default_factory=uuid4) + version: Optional[int] = None + global_position: Optional[int] = None + created_at: datetime = field(default_factory=datetime.utcnow) + + +class EventStore: + def __init__(self, pool: asyncpg.Pool): + self.pool = pool + + async def append_events( + self, + stream_id: str, + stream_type: str, + events: List[Event], + expected_version: Optional[int] = None + ) -> List[Event]: + """Append events to a stream with optimistic concurrency.""" + async with self.pool.acquire() as conn: + async with conn.transaction(): + # Check expected version + if expected_version is not None: + current = await conn.fetchval( + "SELECT MAX(version) FROM events WHERE stream_id = $1", + stream_id + ) + current = current or 0 + if current != expected_version: + raise ConcurrencyError( + f"Expected version {expected_version}, got {current}" + ) + + # Get starting version + start_version = await conn.fetchval( + "SELECT COALESCE(MAX(version), 0) + 1 FROM events WHERE stream_id = $1", + stream_id + ) + + # Insert events + saved_events = [] + for i, event in enumerate(events): + event.version = start_version + i + row = await conn.fetchrow( + """ + INSERT INTO events (id, stream_id, stream_type, event_type, + event_data, metadata, version, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING global_position + """, + event.event_id, + stream_id, + stream_type, + event.event_type, + json.dumps(event.data), + json.dumps(event.metadata), + event.version, + event.created_at + ) + event.global_position = row['global_position'] + saved_events.append(event) + + return saved_events + + async def read_stream( + self, + stream_id: str, + from_version: int = 0, + limit: int = 1000 + ) -> List[Event]: + """Read events from a stream.""" + async with self.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, stream_id, event_type, event_data, metadata, + version, global_position, created_at + FROM events + WHERE stream_id = $1 AND version >= $2 + ORDER BY version + LIMIT $3 + """, + stream_id, from_version, limit + ) + return [self._row_to_event(row) for row in rows] + + async def read_all( + self, + from_position: int = 0, + limit: int = 1000 + ) -> List[Event]: + """Read all events globally.""" + async with self.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, stream_id, event_type, event_data, metadata, + version, global_position, created_at + FROM events + WHERE global_position > $1 + ORDER BY global_position + LIMIT $2 + """, + from_position, limit + ) + return [self._row_to_event(row) for row in rows] + + async def subscribe( + self, + subscription_id: str, + handler, + from_position: int = 0, + batch_size: int = 100 + ): + """Subscribe to all events from a position.""" + # Get checkpoint + async with self.pool.acquire() as conn: + checkpoint = await conn.fetchval( + """ + SELECT last_position FROM subscription_checkpoints + WHERE subscription_id = $1 + """, + subscription_id + ) + position = checkpoint or from_position + + while True: + events = await self.read_all(position, batch_size) + if not events: + await asyncio.sleep(1) # Poll interval + continue + + for event in events: + await handler(event) + position = event.global_position + + # Save checkpoint + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO subscription_checkpoints (subscription_id, last_position) + VALUES ($1, $2) + ON CONFLICT (subscription_id) + DO UPDATE SET last_position = $2, updated_at = NOW() + """, + subscription_id, position + ) + + def _row_to_event(self, row) -> Event: + return Event( + event_id=row['id'], + stream_id=row['stream_id'], + event_type=row['event_type'], + data=json.loads(row['event_data']), + metadata=json.loads(row['metadata']), + version=row['version'], + global_position=row['global_position'], + created_at=row['created_at'] + ) + + +class ConcurrencyError(Exception): + """Raised when optimistic concurrency check fails.""" + pass +``` + +### Template 3: EventStoreDB Usage + +```python +from esdbclient import EventStoreDBClient, NewEvent, StreamState +import json + +# Connect +client = EventStoreDBClient(uri="esdb://localhost:2113?tls=false") + +# Append events +def append_events(stream_name: str, events: list, expected_revision=None): + new_events = [ + NewEvent( + type=event['type'], + data=json.dumps(event['data']).encode(), + metadata=json.dumps(event.get('metadata', {})).encode() + ) + for event in events + ] + + if expected_revision is None: + state = StreamState.ANY + elif expected_revision == -1: + state = StreamState.NO_STREAM + else: + state = expected_revision + + return client.append_to_stream( + stream_name=stream_name, + events=new_events, + current_version=state + ) + +# Read stream +def read_stream(stream_name: str, from_revision: int = 0): + events = client.get_stream( + stream_name=stream_name, + stream_position=from_revision + ) + return [ + { + 'type': event.type, + 'data': json.loads(event.data), + 'metadata': json.loads(event.metadata) if event.metadata else {}, + 'stream_position': event.stream_position, + 'commit_position': event.commit_position + } + for event in events + ] + +# Subscribe to all +async def subscribe_to_all(handler, from_position: int = 0): + subscription = client.subscribe_to_all(commit_position=from_position) + async for event in subscription: + await handler({ + 'type': event.type, + 'data': json.loads(event.data), + 'stream_id': event.stream_name, + 'position': event.commit_position + }) + +# Category projection ($ce-Category) +def read_category(category: str): + """Read all events for a category using system projection.""" + return read_stream(f"$ce-{category}") +``` + +### Template 4: DynamoDB Event Store + +```python +import boto3 +from boto3.dynamodb.conditions import Key +from datetime import datetime +import json +import uuid + +class DynamoEventStore: + def __init__(self, table_name: str): + self.dynamodb = boto3.resource('dynamodb') + self.table = self.dynamodb.Table(table_name) + + def append_events(self, stream_id: str, events: list, expected_version: int = None): + """Append events with conditional write for concurrency.""" + with self.table.batch_writer() as batch: + for i, event in enumerate(events): + version = (expected_version or 0) + i + 1 + item = { + 'PK': f"STREAM#{stream_id}", + 'SK': f"VERSION#{version:020d}", + 'GSI1PK': 'EVENTS', + 'GSI1SK': datetime.utcnow().isoformat(), + 'event_id': str(uuid.uuid4()), + 'stream_id': stream_id, + 'event_type': event['type'], + 'event_data': json.dumps(event['data']), + 'version': version, + 'created_at': datetime.utcnow().isoformat() + } + batch.put_item(Item=item) + return events + + def read_stream(self, stream_id: str, from_version: int = 0): + """Read events from a stream.""" + response = self.table.query( + KeyConditionExpression=Key('PK').eq(f"STREAM#{stream_id}") & + Key('SK').gte(f"VERSION#{from_version:020d}") + ) + return [ + { + 'event_type': item['event_type'], + 'data': json.loads(item['event_data']), + 'version': item['version'] + } + for item in response['Items'] + ] + +# Table definition (CloudFormation/Terraform) +""" +DynamoDB Table: + - PK (Partition Key): String + - SK (Sort Key): String + - GSI1PK, GSI1SK for global ordering + +Capacity: On-demand or provisioned based on throughput needs +""" +``` + +## Best Practices + +### Do's +- **Use stream IDs that include aggregate type** - `Order-{uuid}` +- **Include correlation/causation IDs** - For tracing +- **Version events from day one** - Plan for schema evolution +- **Implement idempotency** - Use event IDs for deduplication +- **Index appropriately** - For your query patterns + +### Don'ts +- **Don't update or delete events** - They're immutable facts +- **Don't store large payloads** - Keep events small +- **Don't skip optimistic concurrency** - Prevents data corruption +- **Don't ignore backpressure** - Handle slow consumers + +## Resources + +- [EventStoreDB](https://www.eventstore.com/) +- [Marten Events](https://martendb.io/events/) +- [Event Sourcing Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing) diff --git a/plugins/backend-development/skills/projection-patterns/SKILL.md b/plugins/backend-development/skills/projection-patterns/SKILL.md new file mode 100644 index 0000000..2b2b6fa --- /dev/null +++ b/plugins/backend-development/skills/projection-patterns/SKILL.md @@ -0,0 +1,488 @@ +--- +name: projection-patterns +description: Build read models and projections from event streams. Use when implementing CQRS read sides, building materialized views, or optimizing query performance in event-sourced systems. +--- + +# Projection Patterns + +Comprehensive guide to building projections and read models for event-sourced systems. + +## When to Use This Skill + +- Building CQRS read models +- Creating materialized views from events +- Optimizing query performance +- Implementing real-time dashboards +- Building search indexes from events +- Aggregating data across streams + +## Core Concepts + +### 1. Projection Architecture + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Event Store │────►│ Projector │────►│ Read Model │ +│ │ │ │ │ (Database) │ +│ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ +│ │ Events │ │ │ │ Handler │ │ │ │ Tables │ │ +│ └─────────┘ │ │ │ Logic │ │ │ │ Views │ │ +│ │ │ └─────────┘ │ │ │ Cache │ │ +└─────────────┘ └─────────────┘ └─────────────┘ +``` + +### 2. Projection Types + +| Type | Description | Use Case | +|------|-------------|----------| +| **Live** | Real-time from subscription | Current state queries | +| **Catchup** | Process historical events | Rebuilding read models | +| **Persistent** | Stores checkpoint | Resume after restart | +| **Inline** | Same transaction as write | Strong consistency | + +## Templates + +### Template 1: Basic Projector + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Dict, Any, Callable, List +import asyncpg + +@dataclass +class Event: + stream_id: str + event_type: str + data: dict + version: int + global_position: int + + +class Projection(ABC): + """Base class for projections.""" + + @property + @abstractmethod + def name(self) -> str: + """Unique projection name for checkpointing.""" + pass + + @abstractmethod + def handles(self) -> List[str]: + """List of event types this projection handles.""" + pass + + @abstractmethod + async def apply(self, event: Event) -> None: + """Apply event to the read model.""" + pass + + +class Projector: + """Runs projections from event store.""" + + def __init__(self, event_store, checkpoint_store): + self.event_store = event_store + self.checkpoint_store = checkpoint_store + self.projections: List[Projection] = [] + + def register(self, projection: Projection): + self.projections.append(projection) + + async def run(self, batch_size: int = 100): + """Run all projections continuously.""" + while True: + for projection in self.projections: + await self._run_projection(projection, batch_size) + await asyncio.sleep(0.1) + + async def _run_projection(self, projection: Projection, batch_size: int): + checkpoint = await self.checkpoint_store.get(projection.name) + position = checkpoint or 0 + + events = await self.event_store.read_all(position, batch_size) + + for event in events: + if event.event_type in projection.handles(): + await projection.apply(event) + + await self.checkpoint_store.save( + projection.name, + event.global_position + ) + + async def rebuild(self, projection: Projection): + """Rebuild a projection from scratch.""" + await self.checkpoint_store.delete(projection.name) + # Optionally clear read model tables + await self._run_projection(projection, batch_size=1000) +``` + +### Template 2: Order Summary Projection + +```python +class OrderSummaryProjection(Projection): + """Projects order events to a summary read model.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "order_summary" + + def handles(self) -> List[str]: + return [ + "OrderCreated", + "OrderItemAdded", + "OrderItemRemoved", + "OrderShipped", + "OrderCompleted", + "OrderCancelled" + ] + + async def apply(self, event: Event) -> None: + handlers = { + "OrderCreated": self._handle_created, + "OrderItemAdded": self._handle_item_added, + "OrderItemRemoved": self._handle_item_removed, + "OrderShipped": self._handle_shipped, + "OrderCompleted": self._handle_completed, + "OrderCancelled": self._handle_cancelled, + } + + handler = handlers.get(event.event_type) + if handler: + await handler(event) + + async def _handle_created(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO order_summaries + (order_id, customer_id, status, total_amount, item_count, created_at) + VALUES ($1, $2, $3, $4, $5, $6) + """, + event.data['order_id'], + event.data['customer_id'], + 'pending', + 0, + 0, + event.data['created_at'] + ) + + async def _handle_item_added(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET total_amount = total_amount + $2, + item_count = item_count + 1, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['price'] * event.data['quantity'] + ) + + async def _handle_item_removed(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET total_amount = total_amount - $2, + item_count = item_count - 1, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['price'] * event.data['quantity'] + ) + + async def _handle_shipped(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'shipped', + shipped_at = $2, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['shipped_at'] + ) + + async def _handle_completed(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'completed', + completed_at = $2, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['completed_at'] + ) + + async def _handle_cancelled(self, event: Event): + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE order_summaries + SET status = 'cancelled', + cancelled_at = $2, + cancellation_reason = $3, + updated_at = NOW() + WHERE order_id = $1 + """, + event.data['order_id'], + event.data['cancelled_at'], + event.data.get('reason') + ) +``` + +### Template 3: Elasticsearch Search Projection + +```python +from elasticsearch import AsyncElasticsearch + +class ProductSearchProjection(Projection): + """Projects product events to Elasticsearch for full-text search.""" + + def __init__(self, es_client: AsyncElasticsearch): + self.es = es_client + self.index = "products" + + @property + def name(self) -> str: + return "product_search" + + def handles(self) -> List[str]: + return [ + "ProductCreated", + "ProductUpdated", + "ProductPriceChanged", + "ProductDeleted" + ] + + async def apply(self, event: Event) -> None: + if event.event_type == "ProductCreated": + await self.es.index( + index=self.index, + id=event.data['product_id'], + document={ + 'name': event.data['name'], + 'description': event.data['description'], + 'category': event.data['category'], + 'price': event.data['price'], + 'tags': event.data.get('tags', []), + 'created_at': event.data['created_at'] + } + ) + + elif event.event_type == "ProductUpdated": + await self.es.update( + index=self.index, + id=event.data['product_id'], + doc={ + 'name': event.data['name'], + 'description': event.data['description'], + 'category': event.data['category'], + 'tags': event.data.get('tags', []), + 'updated_at': event.data['updated_at'] + } + ) + + elif event.event_type == "ProductPriceChanged": + await self.es.update( + index=self.index, + id=event.data['product_id'], + doc={ + 'price': event.data['new_price'], + 'price_updated_at': event.data['changed_at'] + } + ) + + elif event.event_type == "ProductDeleted": + await self.es.delete( + index=self.index, + id=event.data['product_id'] + ) +``` + +### Template 4: Aggregating Projection + +```python +class DailySalesProjection(Projection): + """Aggregates sales data by day for reporting.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "daily_sales" + + def handles(self) -> List[str]: + return ["OrderCompleted", "OrderRefunded"] + + async def apply(self, event: Event) -> None: + if event.event_type == "OrderCompleted": + await self._increment_sales(event) + elif event.event_type == "OrderRefunded": + await self._decrement_sales(event) + + async def _increment_sales(self, event: Event): + date = event.data['completed_at'][:10] # YYYY-MM-DD + async with self.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO daily_sales (date, total_orders, total_revenue, total_items) + VALUES ($1, 1, $2, $3) + ON CONFLICT (date) DO UPDATE SET + total_orders = daily_sales.total_orders + 1, + total_revenue = daily_sales.total_revenue + $2, + total_items = daily_sales.total_items + $3, + updated_at = NOW() + """, + date, + event.data['total_amount'], + event.data['item_count'] + ) + + async def _decrement_sales(self, event: Event): + date = event.data['original_completed_at'][:10] + async with self.pool.acquire() as conn: + await conn.execute( + """ + UPDATE daily_sales SET + total_orders = total_orders - 1, + total_revenue = total_revenue - $2, + total_refunds = total_refunds + $2, + updated_at = NOW() + WHERE date = $1 + """, + date, + event.data['refund_amount'] + ) +``` + +### Template 5: Multi-Table Projection + +```python +class CustomerActivityProjection(Projection): + """Projects customer activity across multiple tables.""" + + def __init__(self, db_pool: asyncpg.Pool): + self.pool = db_pool + + @property + def name(self) -> str: + return "customer_activity" + + def handles(self) -> List[str]: + return [ + "CustomerCreated", + "OrderCompleted", + "ReviewSubmitted", + "CustomerTierChanged" + ] + + async def apply(self, event: Event) -> None: + async with self.pool.acquire() as conn: + async with conn.transaction(): + if event.event_type == "CustomerCreated": + # Insert into customers table + await conn.execute( + """ + INSERT INTO customers (customer_id, email, name, tier, created_at) + VALUES ($1, $2, $3, 'bronze', $4) + """, + event.data['customer_id'], + event.data['email'], + event.data['name'], + event.data['created_at'] + ) + # Initialize activity summary + await conn.execute( + """ + INSERT INTO customer_activity_summary + (customer_id, total_orders, total_spent, total_reviews) + VALUES ($1, 0, 0, 0) + """, + event.data['customer_id'] + ) + + elif event.event_type == "OrderCompleted": + # Update activity summary + await conn.execute( + """ + UPDATE customer_activity_summary SET + total_orders = total_orders + 1, + total_spent = total_spent + $2, + last_order_at = $3 + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['total_amount'], + event.data['completed_at'] + ) + # Insert into order history + await conn.execute( + """ + INSERT INTO customer_order_history + (customer_id, order_id, amount, completed_at) + VALUES ($1, $2, $3, $4) + """, + event.data['customer_id'], + event.data['order_id'], + event.data['total_amount'], + event.data['completed_at'] + ) + + elif event.event_type == "ReviewSubmitted": + await conn.execute( + """ + UPDATE customer_activity_summary SET + total_reviews = total_reviews + 1, + last_review_at = $2 + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['submitted_at'] + ) + + elif event.event_type == "CustomerTierChanged": + await conn.execute( + """ + UPDATE customers SET tier = $2, updated_at = NOW() + WHERE customer_id = $1 + """, + event.data['customer_id'], + event.data['new_tier'] + ) +``` + +## Best Practices + +### Do's +- **Make projections idempotent** - Safe to replay +- **Use transactions** - For multi-table updates +- **Store checkpoints** - Resume after failures +- **Monitor lag** - Alert on projection delays +- **Plan for rebuilds** - Design for reconstruction + +### Don'ts +- **Don't couple projections** - Each is independent +- **Don't skip error handling** - Log and alert on failures +- **Don't ignore ordering** - Events must be processed in order +- **Don't over-normalize** - Denormalize for query patterns + +## Resources + +- [CQRS Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) +- [Projection Building Blocks](https://zimarev.com/blog/event-sourcing/projections/) diff --git a/plugins/backend-development/skills/saga-orchestration/SKILL.md b/plugins/backend-development/skills/saga-orchestration/SKILL.md new file mode 100644 index 0000000..2823cb9 --- /dev/null +++ b/plugins/backend-development/skills/saga-orchestration/SKILL.md @@ -0,0 +1,482 @@ +--- +name: saga-orchestration +description: Implement saga patterns for distributed transactions and cross-aggregate workflows. Use when coordinating multi-step business processes, handling compensating transactions, or managing long-running workflows. +--- + +# Saga Orchestration + +Patterns for managing distributed transactions and long-running business processes. + +## When to Use This Skill + +- Coordinating multi-service transactions +- Implementing compensating transactions +- Managing long-running business workflows +- Handling failures in distributed systems +- Building order fulfillment processes +- Implementing approval workflows + +## Core Concepts + +### 1. Saga Types + +``` +Choreography Orchestration +┌─────┐ ┌─────┐ ┌─────┐ ┌─────────────┐ +│Svc A│─►│Svc B│─►│Svc C│ │ Orchestrator│ +└─────┘ └─────┘ └─────┘ └──────┬──────┘ + │ │ │ │ + ▼ ▼ ▼ ┌─────┼─────┐ + Event Event Event ▼ ▼ ▼ + ┌────┐┌────┐┌────┐ + │Svc1││Svc2││Svc3│ + └────┘└────┘└────┘ +``` + +### 2. Saga Execution States + +| State | Description | +|-------|-------------| +| **Started** | Saga initiated | +| **Pending** | Waiting for step completion | +| **Compensating** | Rolling back due to failure | +| **Completed** | All steps succeeded | +| **Failed** | Saga failed after compensation | + +## Templates + +### Template 1: Saga Orchestrator Base + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from enum import Enum +from typing import List, Dict, Any, Optional +from datetime import datetime +import uuid + +class SagaState(Enum): + STARTED = "started" + PENDING = "pending" + COMPENSATING = "compensating" + COMPLETED = "completed" + FAILED = "failed" + + +@dataclass +class SagaStep: + name: str + action: str + compensation: str + status: str = "pending" + result: Optional[Dict] = None + error: Optional[str] = None + executed_at: Optional[datetime] = None + compensated_at: Optional[datetime] = None + + +@dataclass +class Saga: + saga_id: str + saga_type: str + state: SagaState + data: Dict[str, Any] + steps: List[SagaStep] + current_step: int = 0 + created_at: datetime = field(default_factory=datetime.utcnow) + updated_at: datetime = field(default_factory=datetime.utcnow) + + +class SagaOrchestrator(ABC): + """Base class for saga orchestrators.""" + + def __init__(self, saga_store, event_publisher): + self.saga_store = saga_store + self.event_publisher = event_publisher + + @abstractmethod + def define_steps(self, data: Dict) -> List[SagaStep]: + """Define the saga steps.""" + pass + + @property + @abstractmethod + def saga_type(self) -> str: + """Unique saga type identifier.""" + pass + + async def start(self, data: Dict) -> Saga: + """Start a new saga.""" + saga = Saga( + saga_id=str(uuid.uuid4()), + saga_type=self.saga_type, + state=SagaState.STARTED, + data=data, + steps=self.define_steps(data) + ) + await self.saga_store.save(saga) + await self._execute_next_step(saga) + return saga + + async def handle_step_completed(self, saga_id: str, step_name: str, result: Dict): + """Handle successful step completion.""" + saga = await self.saga_store.get(saga_id) + + # Update step + for step in saga.steps: + if step.name == step_name: + step.status = "completed" + step.result = result + step.executed_at = datetime.utcnow() + break + + saga.current_step += 1 + saga.updated_at = datetime.utcnow() + + # Check if saga is complete + if saga.current_step >= len(saga.steps): + saga.state = SagaState.COMPLETED + await self.saga_store.save(saga) + await self._on_saga_completed(saga) + else: + saga.state = SagaState.PENDING + await self.saga_store.save(saga) + await self._execute_next_step(saga) + + async def handle_step_failed(self, saga_id: str, step_name: str, error: str): + """Handle step failure - start compensation.""" + saga = await self.saga_store.get(saga_id) + + # Mark step as failed + for step in saga.steps: + if step.name == step_name: + step.status = "failed" + step.error = error + break + + saga.state = SagaState.COMPENSATING + saga.updated_at = datetime.utcnow() + await self.saga_store.save(saga) + + # Start compensation from current step backwards + await self._compensate(saga) + + async def _execute_next_step(self, saga: Saga): + """Execute the next step in the saga.""" + if saga.current_step >= len(saga.steps): + return + + step = saga.steps[saga.current_step] + step.status = "executing" + await self.saga_store.save(saga) + + # Publish command to execute step + await self.event_publisher.publish( + step.action, + { + "saga_id": saga.saga_id, + "step_name": step.name, + **saga.data + } + ) + + async def _compensate(self, saga: Saga): + """Execute compensation for completed steps.""" + # Compensate in reverse order + for i in range(saga.current_step - 1, -1, -1): + step = saga.steps[i] + if step.status == "completed": + step.status = "compensating" + await self.saga_store.save(saga) + + await self.event_publisher.publish( + step.compensation, + { + "saga_id": saga.saga_id, + "step_name": step.name, + "original_result": step.result, + **saga.data + } + ) + + async def handle_compensation_completed(self, saga_id: str, step_name: str): + """Handle compensation completion.""" + saga = await self.saga_store.get(saga_id) + + for step in saga.steps: + if step.name == step_name: + step.status = "compensated" + step.compensated_at = datetime.utcnow() + break + + # Check if all compensations complete + all_compensated = all( + s.status in ("compensated", "pending", "failed") + for s in saga.steps + ) + + if all_compensated: + saga.state = SagaState.FAILED + await self._on_saga_failed(saga) + + await self.saga_store.save(saga) + + async def _on_saga_completed(self, saga: Saga): + """Called when saga completes successfully.""" + await self.event_publisher.publish( + f"{self.saga_type}Completed", + {"saga_id": saga.saga_id, **saga.data} + ) + + async def _on_saga_failed(self, saga: Saga): + """Called when saga fails after compensation.""" + await self.event_publisher.publish( + f"{self.saga_type}Failed", + {"saga_id": saga.saga_id, "error": "Saga failed", **saga.data} + ) +``` + +### Template 2: Order Fulfillment Saga + +```python +class OrderFulfillmentSaga(SagaOrchestrator): + """Orchestrates order fulfillment across services.""" + + @property + def saga_type(self) -> str: + return "OrderFulfillment" + + def define_steps(self, data: Dict) -> List[SagaStep]: + return [ + SagaStep( + name="reserve_inventory", + action="InventoryService.ReserveItems", + compensation="InventoryService.ReleaseReservation" + ), + SagaStep( + name="process_payment", + action="PaymentService.ProcessPayment", + compensation="PaymentService.RefundPayment" + ), + SagaStep( + name="create_shipment", + action="ShippingService.CreateShipment", + compensation="ShippingService.CancelShipment" + ), + SagaStep( + name="send_confirmation", + action="NotificationService.SendOrderConfirmation", + compensation="NotificationService.SendCancellationNotice" + ) + ] + + +# Usage +async def create_order(order_data: Dict): + saga = OrderFulfillmentSaga(saga_store, event_publisher) + return await saga.start({ + "order_id": order_data["order_id"], + "customer_id": order_data["customer_id"], + "items": order_data["items"], + "payment_method": order_data["payment_method"], + "shipping_address": order_data["shipping_address"] + }) + + +# Event handlers in each service +class InventoryService: + async def handle_reserve_items(self, command: Dict): + try: + # Reserve inventory + reservation = await self.reserve( + command["items"], + command["order_id"] + ) + # Report success + await self.event_publisher.publish( + "SagaStepCompleted", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory", + "result": {"reservation_id": reservation.id} + } + ) + except InsufficientInventoryError as e: + await self.event_publisher.publish( + "SagaStepFailed", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory", + "error": str(e) + } + ) + + async def handle_release_reservation(self, command: Dict): + # Compensating action + await self.release_reservation( + command["original_result"]["reservation_id"] + ) + await self.event_publisher.publish( + "SagaCompensationCompleted", + { + "saga_id": command["saga_id"], + "step_name": "reserve_inventory" + } + ) +``` + +### Template 3: Choreography-Based Saga + +```python +from dataclasses import dataclass +from typing import Dict, Any +import asyncio + +@dataclass +class SagaContext: + """Passed through choreographed saga events.""" + saga_id: str + step: int + data: Dict[str, Any] + completed_steps: list + + +class OrderChoreographySaga: + """Choreography-based saga using events.""" + + def __init__(self, event_bus): + self.event_bus = event_bus + self._register_handlers() + + def _register_handlers(self): + self.event_bus.subscribe("OrderCreated", self._on_order_created) + self.event_bus.subscribe("InventoryReserved", self._on_inventory_reserved) + self.event_bus.subscribe("PaymentProcessed", self._on_payment_processed) + self.event_bus.subscribe("ShipmentCreated", self._on_shipment_created) + + # Compensation handlers + self.event_bus.subscribe("PaymentFailed", self._on_payment_failed) + self.event_bus.subscribe("ShipmentFailed", self._on_shipment_failed) + + async def _on_order_created(self, event: Dict): + """Step 1: Order created, reserve inventory.""" + await self.event_bus.publish("ReserveInventory", { + "saga_id": event["order_id"], + "order_id": event["order_id"], + "items": event["items"] + }) + + async def _on_inventory_reserved(self, event: Dict): + """Step 2: Inventory reserved, process payment.""" + await self.event_bus.publish("ProcessPayment", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "amount": event["total_amount"], + "reservation_id": event["reservation_id"] + }) + + async def _on_payment_processed(self, event: Dict): + """Step 3: Payment done, create shipment.""" + await self.event_bus.publish("CreateShipment", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "payment_id": event["payment_id"] + }) + + async def _on_shipment_created(self, event: Dict): + """Step 4: Complete - send confirmation.""" + await self.event_bus.publish("OrderFulfilled", { + "saga_id": event["saga_id"], + "order_id": event["order_id"], + "tracking_number": event["tracking_number"] + }) + + # Compensation handlers + async def _on_payment_failed(self, event: Dict): + """Payment failed - release inventory.""" + await self.event_bus.publish("ReleaseInventory", { + "saga_id": event["saga_id"], + "reservation_id": event["reservation_id"] + }) + await self.event_bus.publish("OrderFailed", { + "order_id": event["order_id"], + "reason": "Payment failed" + }) + + async def _on_shipment_failed(self, event: Dict): + """Shipment failed - refund payment and release inventory.""" + await self.event_bus.publish("RefundPayment", { + "saga_id": event["saga_id"], + "payment_id": event["payment_id"] + }) + await self.event_bus.publish("ReleaseInventory", { + "saga_id": event["saga_id"], + "reservation_id": event["reservation_id"] + }) +``` + +### Template 4: Saga with Timeouts + +```python +class TimeoutSagaOrchestrator(SagaOrchestrator): + """Saga orchestrator with step timeouts.""" + + def __init__(self, saga_store, event_publisher, scheduler): + super().__init__(saga_store, event_publisher) + self.scheduler = scheduler + + async def _execute_next_step(self, saga: Saga): + if saga.current_step >= len(saga.steps): + return + + step = saga.steps[saga.current_step] + step.status = "executing" + step.timeout_at = datetime.utcnow() + timedelta(minutes=5) + await self.saga_store.save(saga) + + # Schedule timeout check + await self.scheduler.schedule( + f"saga_timeout_{saga.saga_id}_{step.name}", + self._check_timeout, + {"saga_id": saga.saga_id, "step_name": step.name}, + run_at=step.timeout_at + ) + + await self.event_publisher.publish( + step.action, + {"saga_id": saga.saga_id, "step_name": step.name, **saga.data} + ) + + async def _check_timeout(self, data: Dict): + """Check if step has timed out.""" + saga = await self.saga_store.get(data["saga_id"]) + step = next(s for s in saga.steps if s.name == data["step_name"]) + + if step.status == "executing": + # Step timed out - fail it + await self.handle_step_failed( + data["saga_id"], + data["step_name"], + "Step timed out" + ) +``` + +## Best Practices + +### Do's +- **Make steps idempotent** - Safe to retry +- **Design compensations carefully** - They must work +- **Use correlation IDs** - For tracing across services +- **Implement timeouts** - Don't wait forever +- **Log everything** - For debugging failures + +### Don'ts +- **Don't assume instant completion** - Sagas take time +- **Don't skip compensation testing** - Most critical part +- **Don't couple services** - Use async messaging +- **Don't ignore partial failures** - Handle gracefully + +## Resources + +- [Saga Pattern](https://microservices.io/patterns/data/saga.html) +- [Designing Data-Intensive Applications](https://dataintensive.net/) diff --git a/plugins/business-analytics/skills/data-storytelling/SKILL.md b/plugins/business-analytics/skills/data-storytelling/SKILL.md new file mode 100644 index 0000000..4579e51 --- /dev/null +++ b/plugins/business-analytics/skills/data-storytelling/SKILL.md @@ -0,0 +1,423 @@ +--- +name: data-storytelling +description: Transform data into compelling narratives using visualization, context, and persuasive structure. Use when presenting analytics to stakeholders, creating data reports, or building executive presentations. +--- + +# Data Storytelling + +Transform raw data into compelling narratives that drive decisions and inspire action. + +## When to Use This Skill + +- Presenting analytics to executives +- Creating quarterly business reviews +- Building investor presentations +- Writing data-driven reports +- Communicating insights to non-technical audiences +- Making recommendations based on data + +## Core Concepts + +### 1. Story Structure + +``` +Setup → Conflict → Resolution + +Setup: Context and baseline +Conflict: The problem or opportunity +Resolution: Insights and recommendations +``` + +### 2. Narrative Arc + +``` +1. Hook: Grab attention with surprising insight +2. Context: Establish the baseline +3. Rising Action: Build through data points +4. Climax: The key insight +5. Resolution: Recommendations +6. Call to Action: Next steps +``` + +### 3. Three Pillars + +| Pillar | Purpose | Components | +|--------|---------|------------| +| **Data** | Evidence | Numbers, trends, comparisons | +| **Narrative** | Meaning | Context, causation, implications | +| **Visuals** | Clarity | Charts, diagrams, highlights | + +## Story Frameworks + +### Framework 1: The Problem-Solution Story + +```markdown +# Customer Churn Analysis + +## The Hook +"We're losing $2.4M annually to preventable churn." + +## The Context +- Current churn rate: 8.5% (industry average: 5%) +- Average customer lifetime value: $4,800 +- 500 customers churned last quarter + +## The Problem +Analysis of churned customers reveals a pattern: +- 73% churned within first 90 days +- Common factor: < 3 support interactions +- Low feature adoption in first month + +## The Insight +[Show engagement curve visualization] +Customers who don't engage in the first 14 days +are 4x more likely to churn. + +## The Solution +1. Implement 14-day onboarding sequence +2. Proactive outreach at day 7 +3. Feature adoption tracking + +## Expected Impact +- Reduce early churn by 40% +- Save $960K annually +- Payback period: 3 months + +## Call to Action +Approve $50K budget for onboarding automation. +``` + +### Framework 2: The Trend Story + +```markdown +# Q4 Performance Analysis + +## Where We Started +Q3 ended with $1.2M MRR, 15% below target. +Team morale was low after missed goals. + +## What Changed +[Timeline visualization] +- Oct: Launched self-serve pricing +- Nov: Reduced friction in signup +- Dec: Added customer success calls + +## The Transformation +[Before/after comparison chart] +| Metric | Q3 | Q4 | Change | +|----------------|--------|--------|--------| +| Trial → Paid | 8% | 15% | +87% | +| Time to Value | 14 days| 5 days | -64% | +| Expansion Rate | 2% | 8% | +300% | + +## Key Insight +Self-serve + high-touch creates compound growth. +Customers who self-serve AND get a success call +have 3x higher expansion rate. + +## Going Forward +Double down on hybrid model. +Target: $1.8M MRR by Q2. +``` + +### Framework 3: The Comparison Story + +```markdown +# Market Opportunity Analysis + +## The Question +Should we expand into EMEA or APAC first? + +## The Comparison +[Side-by-side market analysis] + +### EMEA +- Market size: $4.2B +- Growth rate: 8% +- Competition: High +- Regulatory: Complex (GDPR) +- Language: Multiple + +### APAC +- Market size: $3.8B +- Growth rate: 15% +- Competition: Moderate +- Regulatory: Varied +- Language: Multiple + +## The Analysis +[Weighted scoring matrix visualization] + +| Factor | Weight | EMEA Score | APAC Score | +|-------------|--------|------------|------------| +| Market Size | 25% | 5 | 4 | +| Growth | 30% | 3 | 5 | +| Competition | 20% | 2 | 4 | +| Ease | 25% | 2 | 3 | +| **Total** | | **2.9** | **4.1** | + +## The Recommendation +APAC first. Higher growth, less competition. +Start with Singapore hub (English, business-friendly). +Enter EMEA in Year 2 with localization ready. + +## Risk Mitigation +- Timezone coverage: Hire 24/7 support +- Cultural fit: Local partnerships +- Payment: Multi-currency from day 1 +``` + +## Visualization Techniques + +### Technique 1: Progressive Reveal + +```markdown +Start simple, add layers: + +Slide 1: "Revenue is growing" [single line chart] +Slide 2: "But growth is slowing" [add growth rate overlay] +Slide 3: "Driven by one segment" [add segment breakdown] +Slide 4: "Which is saturating" [add market share] +Slide 5: "We need new segments" [add opportunity zones] +``` + +### Technique 2: Contrast and Compare + +```markdown +Before/After: +┌─────────────────┬─────────────────┐ +│ BEFORE │ AFTER │ +│ │ │ +│ Process: 5 days│ Process: 1 day │ +│ Errors: 15% │ Errors: 2% │ +│ Cost: $50/unit │ Cost: $20/unit │ +└─────────────────┴─────────────────┘ + +This/That (emphasize difference): +┌─────────────────────────────────────┐ +│ CUSTOMER A vs B │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ ████████ │ │ ██ │ │ +│ │ $45,000 │ │ $8,000 │ │ +│ │ LTV │ │ LTV │ │ +│ └──────────┘ └──────────┘ │ +│ Onboarded No onboarding │ +└─────────────────────────────────────┘ +``` + +### Technique 3: Annotation and Highlight + +```python +import matplotlib.pyplot as plt +import pandas as pd + +fig, ax = plt.subplots(figsize=(12, 6)) + +# Plot the main data +ax.plot(dates, revenue, linewidth=2, color='#2E86AB') + +# Add annotation for key events +ax.annotate( + 'Product Launch\n+32% spike', + xy=(launch_date, launch_revenue), + xytext=(launch_date, launch_revenue * 1.2), + fontsize=10, + arrowprops=dict(arrowstyle='->', color='#E63946'), + color='#E63946' +) + +# Highlight a region +ax.axvspan(growth_start, growth_end, alpha=0.2, color='green', + label='Growth Period') + +# Add threshold line +ax.axhline(y=target, color='gray', linestyle='--', + label=f'Target: ${target:,.0f}') + +ax.set_title('Revenue Growth Story', fontsize=14, fontweight='bold') +ax.legend() +``` + +## Presentation Templates + +### Template 1: Executive Summary Slide + +``` +┌─────────────────────────────────────────────────────────────┐ +│ KEY INSIGHT │ +│ ══════════════════════════════════════════════════════════│ +│ │ +│ "Customers who complete onboarding in week 1 │ +│ have 3x higher lifetime value" │ +│ │ +├──────────────────────┬──────────────────────────────────────┤ +│ │ │ +│ THE DATA │ THE IMPLICATION │ +│ │ │ +│ Week 1 completers: │ ✓ Prioritize onboarding UX │ +│ • LTV: $4,500 │ ✓ Add day-1 success milestones │ +│ • Retention: 85% │ ✓ Proactive week-1 outreach │ +│ • NPS: 72 │ │ +│ │ Investment: $75K │ +│ Others: │ Expected ROI: 8x │ +│ • LTV: $1,500 │ │ +│ • Retention: 45% │ │ +│ • NPS: 34 │ │ +│ │ │ +└──────────────────────┴──────────────────────────────────────┘ +``` + +### Template 2: Data Story Flow + +``` +Slide 1: THE HEADLINE +"We can grow 40% faster by fixing onboarding" + +Slide 2: THE CONTEXT +Current state metrics +Industry benchmarks +Gap analysis + +Slide 3: THE DISCOVERY +What the data revealed +Surprising finding +Pattern identification + +Slide 4: THE DEEP DIVE +Root cause analysis +Segment breakdowns +Statistical significance + +Slide 5: THE RECOMMENDATION +Proposed actions +Resource requirements +Timeline + +Slide 6: THE IMPACT +Expected outcomes +ROI calculation +Risk assessment + +Slide 7: THE ASK +Specific request +Decision needed +Next steps +``` + +### Template 3: One-Page Dashboard Story + +```markdown +# Monthly Business Review: January 2024 + +## THE HEADLINE +Revenue up 15% but CAC increasing faster than LTV + +## KEY METRICS AT A GLANCE +┌────────┬────────┬────────┬────────┐ +│ MRR │ NRR │ CAC │ LTV │ +│ $125K │ 108% │ $450 │ $2,200 │ +│ ▲15% │ ▲3% │ ▲22% │ ▲8% │ +└────────┴────────┴────────┴────────┘ + +## WHAT'S WORKING +✓ Enterprise segment growing 25% MoM +✓ Referral program driving 30% of new logos +✓ Support satisfaction at all-time high (94%) + +## WHAT NEEDS ATTENTION +✗ SMB acquisition cost up 40% +✗ Trial conversion down 5 points +✗ Time-to-value increased by 3 days + +## ROOT CAUSE +[Mini chart showing SMB vs Enterprise CAC trend] +SMB paid ads becoming less efficient. +CPC up 35% while conversion flat. + +## RECOMMENDATION +1. Shift $20K/mo from paid to content +2. Launch SMB self-serve trial +3. A/B test shorter onboarding + +## NEXT MONTH'S FOCUS +- Launch content marketing pilot +- Complete self-serve MVP +- Reduce time-to-value to < 7 days +``` + +## Writing Techniques + +### Headlines That Work + +```markdown +BAD: "Q4 Sales Analysis" +GOOD: "Q4 Sales Beat Target by 23% - Here's Why" + +BAD: "Customer Churn Report" +GOOD: "We're Losing $2.4M to Preventable Churn" + +BAD: "Marketing Performance" +GOOD: "Content Marketing Delivers 4x ROI vs. Paid" + +Formula: +[Specific Number] + [Business Impact] + [Actionable Context] +``` + +### Transition Phrases + +```markdown +Building the narrative: +• "This leads us to ask..." +• "When we dig deeper..." +• "The pattern becomes clear when..." +• "Contrast this with..." + +Introducing insights: +• "The data reveals..." +• "What surprised us was..." +• "The inflection point came when..." +• "The key finding is..." + +Moving to action: +• "This insight suggests..." +• "Based on this analysis..." +• "The implication is clear..." +• "Our recommendation is..." +``` + +### Handling Uncertainty + +```markdown +Acknowledge limitations: +• "With 95% confidence, we can say..." +• "The sample size of 500 shows..." +• "While correlation is strong, causation requires..." +• "This trend holds for [segment], though [caveat]..." + +Present ranges: +• "Impact estimate: $400K-$600K" +• "Confidence interval: 15-20% improvement" +• "Best case: X, Conservative: Y" +``` + +## Best Practices + +### Do's +- **Start with the "so what"** - Lead with insight +- **Use the rule of three** - Three points, three comparisons +- **Show, don't tell** - Let data speak +- **Make it personal** - Connect to audience goals +- **End with action** - Clear next steps + +### Don'ts +- **Don't data dump** - Curate ruthlessly +- **Don't bury the insight** - Front-load key findings +- **Don't use jargon** - Match audience vocabulary +- **Don't show methodology first** - Context, then method +- **Don't forget the narrative** - Numbers need meaning + +## Resources + +- [Storytelling with Data (Cole Nussbaumer)](https://www.storytellingwithdata.com/) +- [The Pyramid Principle (Barbara Minto)](https://www.amazon.com/Pyramid-Principle-Logic-Writing-Thinking/dp/0273710516) +- [Resonate (Nancy Duarte)](https://www.duarte.com/resonate/) diff --git a/plugins/business-analytics/skills/kpi-dashboard-design/SKILL.md b/plugins/business-analytics/skills/kpi-dashboard-design/SKILL.md new file mode 100644 index 0000000..0e839db --- /dev/null +++ b/plugins/business-analytics/skills/kpi-dashboard-design/SKILL.md @@ -0,0 +1,426 @@ +--- +name: kpi-dashboard-design +description: Design effective KPI dashboards with metrics selection, visualization best practices, and real-time monitoring patterns. Use when building business dashboards, selecting metrics, or designing data visualization layouts. +--- + +# KPI Dashboard Design + +Comprehensive patterns for designing effective Key Performance Indicator (KPI) dashboards that drive business decisions. + +## When to Use This Skill + +- Designing executive dashboards +- Selecting meaningful KPIs +- Building real-time monitoring displays +- Creating department-specific metrics views +- Improving existing dashboard layouts +- Establishing metric governance + +## Core Concepts + +### 1. KPI Framework + +| Level | Focus | Update Frequency | Audience | +|-------|-------|------------------|----------| +| **Strategic** | Long-term goals | Monthly/Quarterly | Executives | +| **Tactical** | Department goals | Weekly/Monthly | Managers | +| **Operational** | Day-to-day | Real-time/Daily | Teams | + +### 2. SMART KPIs + +``` +Specific: Clear definition +Measurable: Quantifiable +Achievable: Realistic targets +Relevant: Aligned to goals +Time-bound: Defined period +``` + +### 3. Dashboard Hierarchy + +``` +├── Executive Summary (1 page) +│ ├── 4-6 headline KPIs +│ ├── Trend indicators +│ └── Key alerts +├── Department Views +│ ├── Sales Dashboard +│ ├── Marketing Dashboard +│ ├── Operations Dashboard +│ └── Finance Dashboard +└── Detailed Drilldowns + ├── Individual metrics + └── Root cause analysis +``` + +## Common KPIs by Department + +### Sales KPIs + +```yaml +Revenue Metrics: + - Monthly Recurring Revenue (MRR) + - Annual Recurring Revenue (ARR) + - Average Revenue Per User (ARPU) + - Revenue Growth Rate + +Pipeline Metrics: + - Sales Pipeline Value + - Win Rate + - Average Deal Size + - Sales Cycle Length + +Activity Metrics: + - Calls/Emails per Rep + - Demos Scheduled + - Proposals Sent + - Close Rate +``` + +### Marketing KPIs + +```yaml +Acquisition: + - Cost Per Acquisition (CPA) + - Customer Acquisition Cost (CAC) + - Lead Volume + - Marketing Qualified Leads (MQL) + +Engagement: + - Website Traffic + - Conversion Rate + - Email Open/Click Rate + - Social Engagement + +ROI: + - Marketing ROI + - Campaign Performance + - Channel Attribution + - CAC Payback Period +``` + +### Product KPIs + +```yaml +Usage: + - Daily/Monthly Active Users (DAU/MAU) + - Session Duration + - Feature Adoption Rate + - Stickiness (DAU/MAU) + +Quality: + - Net Promoter Score (NPS) + - Customer Satisfaction (CSAT) + - Bug/Issue Count + - Time to Resolution + +Growth: + - User Growth Rate + - Activation Rate + - Retention Rate + - Churn Rate +``` + +### Finance KPIs + +```yaml +Profitability: + - Gross Margin + - Net Profit Margin + - EBITDA + - Operating Margin + +Liquidity: + - Current Ratio + - Quick Ratio + - Cash Flow + - Working Capital + +Efficiency: + - Revenue per Employee + - Operating Expense Ratio + - Days Sales Outstanding + - Inventory Turnover +``` + +## Dashboard Layout Patterns + +### Pattern 1: Executive Summary + +``` +┌─────────────────────────────────────────────────────────────┐ +│ EXECUTIVE DASHBOARD [Date Range ▼] │ +├─────────────┬─────────────┬─────────────┬─────────────────┤ +│ REVENUE │ PROFIT │ CUSTOMERS │ NPS SCORE │ +│ $2.4M │ $450K │ 12,450 │ 72 │ +│ ▲ 12% │ ▲ 8% │ ▲ 15% │ ▲ 5pts │ +├─────────────┴─────────────┴─────────────┴─────────────────┤ +│ │ +│ Revenue Trend │ Revenue by Product │ +│ ┌───────────────────────┐ │ ┌──────────────────┐ │ +│ │ /\ /\ │ │ │ ████████ 45% │ │ +│ │ / \ / \ /\ │ │ │ ██████ 32% │ │ +│ │ / \/ \ / \ │ │ │ ████ 18% │ │ +│ │ / \/ \ │ │ │ ██ 5% │ │ +│ └───────────────────────┘ │ └──────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────┤ +│ 🔴 Alert: Churn rate exceeded threshold (>5%) │ +│ 🟡 Warning: Support ticket volume 20% above average │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Pattern 2: SaaS Metrics Dashboard + +``` +┌─────────────────────────────────────────────────────────────┐ +│ SAAS METRICS Jan 2024 [Monthly ▼] │ +├──────────────────────┬──────────────────────────────────────┤ +│ ┌────────────────┐ │ MRR GROWTH │ +│ │ MRR │ │ ┌────────────────────────────────┐ │ +│ │ $125,000 │ │ │ /── │ │ +│ │ ▲ 8% │ │ │ /────/ │ │ +│ └────────────────┘ │ │ /────/ │ │ +│ ┌────────────────┐ │ │ /────/ │ │ +│ │ ARR │ │ │ /────/ │ │ +│ │ $1,500,000 │ │ └────────────────────────────────┘ │ +│ │ ▲ 15% │ │ J F M A M J J A S O N D │ +│ └────────────────┘ │ │ +├──────────────────────┼──────────────────────────────────────┤ +│ UNIT ECONOMICS │ COHORT RETENTION │ +│ │ │ +│ CAC: $450 │ Month 1: ████████████████████ 100% │ +│ LTV: $2,700 │ Month 3: █████████████████ 85% │ +│ LTV/CAC: 6.0x │ Month 6: ████████████████ 80% │ +│ │ Month 12: ██████████████ 72% │ +│ Payback: 4 months │ │ +├──────────────────────┴──────────────────────────────────────┤ +│ CHURN ANALYSIS │ +│ ┌──────────┬──────────┬──────────┬──────────────────────┐ │ +│ │ Gross │ Net │ Logo │ Expansion │ │ +│ │ 4.2% │ 1.8% │ 3.1% │ 2.4% │ │ +│ └──────────┴──────────┴──────────┴──────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Pattern 3: Real-time Operations + +``` +┌─────────────────────────────────────────────────────────────┐ +│ OPERATIONS CENTER Live ● Last: 10:42:15 │ +├────────────────────────────┬────────────────────────────────┤ +│ SYSTEM HEALTH │ SERVICE STATUS │ +│ ┌──────────────────────┐ │ │ +│ │ CPU MEM DISK │ │ ● API Gateway Healthy │ +│ │ 45% 72% 58% │ │ ● User Service Healthy │ +│ │ ███ ████ ███ │ │ ● Payment Service Degraded │ +│ │ ███ ████ ███ │ │ ● Database Healthy │ +│ │ ███ ████ ███ │ │ ● Cache Healthy │ +│ └──────────────────────┘ │ │ +├────────────────────────────┼────────────────────────────────┤ +│ REQUEST THROUGHPUT │ ERROR RATE │ +│ ┌──────────────────────┐ │ ┌──────────────────────────┐ │ +│ │ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅ │ │ │ ▁▁▁▁▁▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁ │ │ +│ └──────────────────────┘ │ └──────────────────────────┘ │ +│ Current: 12,450 req/s │ Current: 0.02% │ +│ Peak: 18,200 req/s │ Threshold: 1.0% │ +├────────────────────────────┴────────────────────────────────┤ +│ RECENT ALERTS │ +│ 10:40 🟡 High latency on payment-service (p99 > 500ms) │ +│ 10:35 🟢 Resolved: Database connection pool recovered │ +│ 10:22 🔴 Payment service circuit breaker tripped │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Implementation Patterns + +### SQL for KPI Calculations + +```sql +-- Monthly Recurring Revenue (MRR) +WITH mrr_calculation AS ( + SELECT + DATE_TRUNC('month', billing_date) AS month, + SUM( + CASE subscription_interval + WHEN 'monthly' THEN amount + WHEN 'yearly' THEN amount / 12 + WHEN 'quarterly' THEN amount / 3 + END + ) AS mrr + FROM subscriptions + WHERE status = 'active' + GROUP BY DATE_TRUNC('month', billing_date) +) +SELECT + month, + mrr, + LAG(mrr) OVER (ORDER BY month) AS prev_mrr, + (mrr - LAG(mrr) OVER (ORDER BY month)) / LAG(mrr) OVER (ORDER BY month) * 100 AS growth_pct +FROM mrr_calculation; + +-- Cohort Retention +WITH cohorts AS ( + SELECT + user_id, + DATE_TRUNC('month', created_at) AS cohort_month + FROM users +), +activity AS ( + SELECT + user_id, + DATE_TRUNC('month', event_date) AS activity_month + FROM user_events + WHERE event_type = 'active_session' +) +SELECT + c.cohort_month, + EXTRACT(MONTH FROM age(a.activity_month, c.cohort_month)) AS months_since_signup, + COUNT(DISTINCT a.user_id) AS active_users, + COUNT(DISTINCT a.user_id)::FLOAT / COUNT(DISTINCT c.user_id) * 100 AS retention_rate +FROM cohorts c +LEFT JOIN activity a ON c.user_id = a.user_id + AND a.activity_month >= c.cohort_month +GROUP BY c.cohort_month, EXTRACT(MONTH FROM age(a.activity_month, c.cohort_month)) +ORDER BY c.cohort_month, months_since_signup; + +-- Customer Acquisition Cost (CAC) +SELECT + DATE_TRUNC('month', acquired_date) AS month, + SUM(marketing_spend) / NULLIF(COUNT(new_customers), 0) AS cac, + SUM(marketing_spend) AS total_spend, + COUNT(new_customers) AS customers_acquired +FROM ( + SELECT + DATE_TRUNC('month', u.created_at) AS acquired_date, + u.id AS new_customers, + m.spend AS marketing_spend + FROM users u + JOIN marketing_spend m ON DATE_TRUNC('month', u.created_at) = m.month + WHERE u.source = 'marketing' +) acquisition +GROUP BY DATE_TRUNC('month', acquired_date); +``` + +### Python Dashboard Code (Streamlit) + +```python +import streamlit as st +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go + +st.set_page_config(page_title="KPI Dashboard", layout="wide") + +# Header with date filter +col1, col2 = st.columns([3, 1]) +with col1: + st.title("Executive Dashboard") +with col2: + date_range = st.selectbox( + "Period", + ["Last 7 Days", "Last 30 Days", "Last Quarter", "YTD"] + ) + +# KPI Cards +def metric_card(label, value, delta, prefix="", suffix=""): + delta_color = "green" if delta >= 0 else "red" + delta_arrow = "▲" if delta >= 0 else "▼" + st.metric( + label=label, + value=f"{prefix}{value:,.0f}{suffix}", + delta=f"{delta_arrow} {abs(delta):.1f}%" + ) + +col1, col2, col3, col4 = st.columns(4) +with col1: + metric_card("Revenue", 2400000, 12.5, prefix="$") +with col2: + metric_card("Customers", 12450, 15.2) +with col3: + metric_card("NPS Score", 72, 5.0) +with col4: + metric_card("Churn Rate", 4.2, -0.8, suffix="%") + +# Charts +col1, col2 = st.columns(2) + +with col1: + st.subheader("Revenue Trend") + revenue_data = pd.DataFrame({ + 'Month': pd.date_range('2024-01-01', periods=12, freq='M'), + 'Revenue': [180000, 195000, 210000, 225000, 240000, 255000, + 270000, 285000, 300000, 315000, 330000, 345000] + }) + fig = px.line(revenue_data, x='Month', y='Revenue', + line_shape='spline', markers=True) + fig.update_layout(height=300) + st.plotly_chart(fig, use_container_width=True) + +with col2: + st.subheader("Revenue by Product") + product_data = pd.DataFrame({ + 'Product': ['Enterprise', 'Professional', 'Starter', 'Other'], + 'Revenue': [45, 32, 18, 5] + }) + fig = px.pie(product_data, values='Revenue', names='Product', + hole=0.4) + fig.update_layout(height=300) + st.plotly_chart(fig, use_container_width=True) + +# Cohort Heatmap +st.subheader("Cohort Retention") +cohort_data = pd.DataFrame({ + 'Cohort': ['Jan', 'Feb', 'Mar', 'Apr', 'May'], + 'M0': [100, 100, 100, 100, 100], + 'M1': [85, 87, 84, 86, 88], + 'M2': [78, 80, 76, 79, None], + 'M3': [72, 74, 70, None, None], + 'M4': [68, 70, None, None, None], +}) +fig = go.Figure(data=go.Heatmap( + z=cohort_data.iloc[:, 1:].values, + x=['M0', 'M1', 'M2', 'M3', 'M4'], + y=cohort_data['Cohort'], + colorscale='Blues', + text=cohort_data.iloc[:, 1:].values, + texttemplate='%{text}%', + textfont={"size": 12}, +)) +fig.update_layout(height=250) +st.plotly_chart(fig, use_container_width=True) + +# Alerts Section +st.subheader("Alerts") +alerts = [ + {"level": "error", "message": "Churn rate exceeded threshold (>5%)"}, + {"level": "warning", "message": "Support ticket volume 20% above average"}, +] +for alert in alerts: + if alert["level"] == "error": + st.error(f"🔴 {alert['message']}") + elif alert["level"] == "warning": + st.warning(f"🟡 {alert['message']}") +``` + +## Best Practices + +### Do's +- **Limit to 5-7 KPIs** - Focus on what matters +- **Show context** - Comparisons, trends, targets +- **Use consistent colors** - Red=bad, green=good +- **Enable drilldown** - From summary to detail +- **Update appropriately** - Match metric frequency + +### Don'ts +- **Don't show vanity metrics** - Focus on actionable data +- **Don't overcrowd** - White space aids comprehension +- **Don't use 3D charts** - They distort perception +- **Don't hide methodology** - Document calculations +- **Don't ignore mobile** - Ensure responsive design + +## Resources + +- [Stephen Few's Dashboard Design](https://www.perceptualedge.com/articles/visual_business_intelligence/rules_for_using_color.pdf) +- [Edward Tufte's Principles](https://www.edwardtufte.com/tufte/) +- [Google Data Studio Gallery](https://datastudio.google.com/gallery) diff --git a/plugins/cloud-infrastructure/agents/service-mesh-expert.md b/plugins/cloud-infrastructure/agents/service-mesh-expert.md new file mode 100644 index 0000000..31ff02a --- /dev/null +++ b/plugins/cloud-infrastructure/agents/service-mesh-expert.md @@ -0,0 +1,41 @@ +# Service Mesh Expert + +Expert service mesh architect specializing in Istio, Linkerd, and cloud-native networking patterns. Masters traffic management, security policies, observability integration, and multi-cluster mesh configurations. Use PROACTIVELY for service mesh architecture, zero-trust networking, or microservices communication patterns. + +## Capabilities + +- Istio and Linkerd installation, configuration, and optimization +- Traffic management: routing, load balancing, circuit breaking, retries +- mTLS configuration and certificate management +- Service mesh observability with distributed tracing +- Multi-cluster and multi-cloud mesh federation +- Progressive delivery with canary and blue-green deployments +- Security policies and authorization rules + +## When to Use + +- Implementing service-to-service communication in Kubernetes +- Setting up zero-trust networking with mTLS +- Configuring traffic splitting for canary deployments +- Debugging service mesh connectivity issues +- Implementing rate limiting and circuit breakers +- Setting up cross-cluster service discovery + +## Workflow + +1. Assess current infrastructure and requirements +2. Design mesh topology and traffic policies +3. Implement security policies (mTLS, AuthorizationPolicy) +4. Configure observability (metrics, traces, logs) +5. Set up traffic management rules +6. Test failover and resilience patterns +7. Document operational runbooks + +## Best Practices + +- Start with permissive mode, gradually enforce strict mTLS +- Use namespaces for policy isolation +- Implement circuit breakers before they're needed +- Monitor mesh overhead (latency, resource usage) +- Keep sidecar resources appropriately sized +- Use destination rules for consistent load balancing diff --git a/plugins/cloud-infrastructure/skills/istio-traffic-management/SKILL.md b/plugins/cloud-infrastructure/skills/istio-traffic-management/SKILL.md new file mode 100644 index 0000000..e6d502c --- /dev/null +++ b/plugins/cloud-infrastructure/skills/istio-traffic-management/SKILL.md @@ -0,0 +1,325 @@ +--- +name: istio-traffic-management +description: Configure Istio traffic management including routing, load balancing, circuit breakers, and canary deployments. Use when implementing service mesh traffic policies, progressive delivery, or resilience patterns. +--- + +# Istio Traffic Management + +Comprehensive guide to Istio traffic management for production service mesh deployments. + +## When to Use This Skill + +- Configuring service-to-service routing +- Implementing canary or blue-green deployments +- Setting up circuit breakers and retries +- Load balancing configuration +- Traffic mirroring for testing +- Fault injection for chaos engineering + +## Core Concepts + +### 1. Traffic Management Resources + +| Resource | Purpose | Scope | +|----------|---------|-------| +| **VirtualService** | Route traffic to destinations | Host-based | +| **DestinationRule** | Define policies after routing | Service-based | +| **Gateway** | Configure ingress/egress | Cluster edge | +| **ServiceEntry** | Add external services | Mesh-wide | + +### 2. Traffic Flow + +``` +Client → Gateway → VirtualService → DestinationRule → Service + (routing) (policies) (pods) +``` + +## Templates + +### Template 1: Basic Routing + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: reviews-route + namespace: bookinfo +spec: + hosts: + - reviews + http: + - match: + - headers: + end-user: + exact: jason + route: + - destination: + host: reviews + subset: v2 + - route: + - destination: + host: reviews + subset: v1 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: reviews-destination + namespace: bookinfo +spec: + host: reviews + subsets: + - name: v1 + labels: + version: v1 + - name: v2 + labels: + version: v2 + - name: v3 + labels: + version: v3 +``` + +### Template 2: Canary Deployment + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: my-service-canary +spec: + hosts: + - my-service + http: + - route: + - destination: + host: my-service + subset: stable + weight: 90 + - destination: + host: my-service + subset: canary + weight: 10 +--- +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: my-service-dr +spec: + host: my-service + trafficPolicy: + connectionPool: + tcp: + maxConnections: 100 + http: + h2UpgradePolicy: UPGRADE + http1MaxPendingRequests: 100 + http2MaxRequests: 1000 + subsets: + - name: stable + labels: + version: stable + - name: canary + labels: + version: canary +``` + +### Template 3: Circuit Breaker + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: circuit-breaker +spec: + host: my-service + trafficPolicy: + connectionPool: + tcp: + maxConnections: 100 + http: + http1MaxPendingRequests: 100 + http2MaxRequests: 1000 + maxRequestsPerConnection: 10 + maxRetries: 3 + outlierDetection: + consecutive5xxErrors: 5 + interval: 30s + baseEjectionTime: 30s + maxEjectionPercent: 50 + minHealthPercent: 30 +``` + +### Template 4: Retry and Timeout + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: ratings-retry +spec: + hosts: + - ratings + http: + - route: + - destination: + host: ratings + timeout: 10s + retries: + attempts: 3 + perTryTimeout: 3s + retryOn: connect-failure,refused-stream,unavailable,cancelled,retriable-4xx,503 + retryRemoteLocalities: true +``` + +### Template 5: Traffic Mirroring + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: mirror-traffic +spec: + hosts: + - my-service + http: + - route: + - destination: + host: my-service + subset: v1 + mirror: + host: my-service + subset: v2 + mirrorPercentage: + value: 100.0 +``` + +### Template 6: Fault Injection + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: fault-injection +spec: + hosts: + - ratings + http: + - fault: + delay: + percentage: + value: 10 + fixedDelay: 5s + abort: + percentage: + value: 5 + httpStatus: 503 + route: + - destination: + host: ratings +``` + +### Template 7: Ingress Gateway + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: my-gateway +spec: + selector: + istio: ingressgateway + servers: + - port: + number: 443 + name: https + protocol: HTTPS + tls: + mode: SIMPLE + credentialName: my-tls-secret + hosts: + - "*.example.com" +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: my-vs +spec: + hosts: + - "api.example.com" + gateways: + - my-gateway + http: + - match: + - uri: + prefix: /api/v1 + route: + - destination: + host: api-service + port: + number: 8080 +``` + +## Load Balancing Strategies + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: load-balancing +spec: + host: my-service + trafficPolicy: + loadBalancer: + simple: ROUND_ROBIN # or LEAST_CONN, RANDOM, PASSTHROUGH +--- +# Consistent hashing for sticky sessions +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: sticky-sessions +spec: + host: my-service + trafficPolicy: + loadBalancer: + consistentHash: + httpHeaderName: x-user-id + # or: httpCookie, useSourceIp, httpQueryParameterName +``` + +## Best Practices + +### Do's +- **Start simple** - Add complexity incrementally +- **Use subsets** - Version your services clearly +- **Set timeouts** - Always configure reasonable timeouts +- **Enable retries** - But with backoff and limits +- **Monitor** - Use Kiali and Jaeger for visibility + +### Don'ts +- **Don't over-retry** - Can cause cascading failures +- **Don't ignore outlier detection** - Enable circuit breakers +- **Don't mirror to production** - Mirror to test environments +- **Don't skip canary** - Test with small traffic percentage first + +## Debugging Commands + +```bash +# Check VirtualService configuration +istioctl analyze + +# View effective routes +istioctl proxy-config routes deploy/my-app -o json + +# Check endpoint discovery +istioctl proxy-config endpoints deploy/my-app + +# Debug traffic +istioctl proxy-config log deploy/my-app --level debug +``` + +## Resources + +- [Istio Traffic Management](https://istio.io/latest/docs/concepts/traffic-management/) +- [Virtual Service Reference](https://istio.io/latest/docs/reference/config/networking/virtual-service/) +- [Destination Rule Reference](https://istio.io/latest/docs/reference/config/networking/destination-rule/) diff --git a/plugins/cloud-infrastructure/skills/linkerd-patterns/SKILL.md b/plugins/cloud-infrastructure/skills/linkerd-patterns/SKILL.md new file mode 100644 index 0000000..2e36543 --- /dev/null +++ b/plugins/cloud-infrastructure/skills/linkerd-patterns/SKILL.md @@ -0,0 +1,309 @@ +--- +name: linkerd-patterns +description: Implement Linkerd service mesh patterns for lightweight, security-focused service mesh deployments. Use when setting up Linkerd, configuring traffic policies, or implementing zero-trust networking with minimal overhead. +--- + +# Linkerd Patterns + +Production patterns for Linkerd service mesh - the lightweight, security-first service mesh for Kubernetes. + +## When to Use This Skill + +- Setting up a lightweight service mesh +- Implementing automatic mTLS +- Configuring traffic splits for canary deployments +- Setting up service profiles for per-route metrics +- Implementing retries and timeouts +- Multi-cluster service mesh + +## Core Concepts + +### 1. Linkerd Architecture + +``` +┌─────────────────────────────────────────────┐ +│ Control Plane │ +│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │ destiny │ │ identity │ │ proxy-inject │ │ +│ └─────────┘ └──────────┘ └──────────────┘ │ +└─────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────┐ +│ Data Plane │ +│ ┌─────┐ ┌─────┐ ┌─────┐ │ +│ │proxy│────│proxy│────│proxy│ │ +│ └─────┘ └─────┘ └─────┘ │ +│ │ │ │ │ +│ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │ +│ │ app │ │ app │ │ app │ │ +│ └─────┘ └─────┘ └─────┘ │ +└─────────────────────────────────────────────┘ +``` + +### 2. Key Resources + +| Resource | Purpose | +|----------|---------| +| **ServiceProfile** | Per-route metrics, retries, timeouts | +| **TrafficSplit** | Canary deployments, A/B testing | +| **Server** | Define server-side policies | +| **ServerAuthorization** | Access control policies | + +## Templates + +### Template 1: Mesh Installation + +```bash +# Install CLI +curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install | sh + +# Validate cluster +linkerd check --pre + +# Install CRDs +linkerd install --crds | kubectl apply -f - + +# Install control plane +linkerd install | kubectl apply -f - + +# Verify installation +linkerd check + +# Install viz extension (optional) +linkerd viz install | kubectl apply -f - +``` + +### Template 2: Inject Namespace + +```yaml +# Automatic injection for namespace +apiVersion: v1 +kind: Namespace +metadata: + name: my-app + annotations: + linkerd.io/inject: enabled +--- +# Or inject specific deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + annotations: + linkerd.io/inject: enabled +spec: + template: + metadata: + annotations: + linkerd.io/inject: enabled +``` + +### Template 3: Service Profile with Retries + +```yaml +apiVersion: linkerd.io/v1alpha2 +kind: ServiceProfile +metadata: + name: my-service.my-namespace.svc.cluster.local + namespace: my-namespace +spec: + routes: + - name: GET /api/users + condition: + method: GET + pathRegex: /api/users + responseClasses: + - condition: + status: + min: 500 + max: 599 + isFailure: true + isRetryable: true + - name: POST /api/users + condition: + method: POST + pathRegex: /api/users + # POST not retryable by default + isRetryable: false + - name: GET /api/users/{id} + condition: + method: GET + pathRegex: /api/users/[^/]+ + timeout: 5s + isRetryable: true + retryBudget: + retryRatio: 0.2 + minRetriesPerSecond: 10 + ttl: 10s +``` + +### Template 4: Traffic Split (Canary) + +```yaml +apiVersion: split.smi-spec.io/v1alpha1 +kind: TrafficSplit +metadata: + name: my-service-canary + namespace: my-namespace +spec: + service: my-service + backends: + - service: my-service-stable + weight: 900m # 90% + - service: my-service-canary + weight: 100m # 10% +``` + +### Template 5: Server Authorization Policy + +```yaml +# Define the server +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + name: my-service-http + namespace: my-namespace +spec: + podSelector: + matchLabels: + app: my-service + port: http + proxyProtocol: HTTP/1 +--- +# Allow traffic from specific clients +apiVersion: policy.linkerd.io/v1beta1 +kind: ServerAuthorization +metadata: + name: allow-frontend + namespace: my-namespace +spec: + server: + name: my-service-http + client: + meshTLS: + serviceAccounts: + - name: frontend + namespace: my-namespace +--- +# Allow unauthenticated traffic (e.g., from ingress) +apiVersion: policy.linkerd.io/v1beta1 +kind: ServerAuthorization +metadata: + name: allow-ingress + namespace: my-namespace +spec: + server: + name: my-service-http + client: + unauthenticated: true + networks: + - cidr: 10.0.0.0/8 +``` + +### Template 6: HTTPRoute for Advanced Routing + +```yaml +apiVersion: policy.linkerd.io/v1beta2 +kind: HTTPRoute +metadata: + name: my-route + namespace: my-namespace +spec: + parentRefs: + - name: my-service + kind: Service + group: core + port: 8080 + rules: + - matches: + - path: + type: PathPrefix + value: /api/v2 + - headers: + - name: x-api-version + value: v2 + backendRefs: + - name: my-service-v2 + port: 8080 + - matches: + - path: + type: PathPrefix + value: /api + backendRefs: + - name: my-service-v1 + port: 8080 +``` + +### Template 7: Multi-cluster Setup + +```bash +# On each cluster, install with cluster credentials +linkerd multicluster install | kubectl apply -f - + +# Link clusters +linkerd multicluster link --cluster-name west \ + --api-server-address https://west.example.com:6443 \ + | kubectl apply -f - + +# Export a service to other clusters +kubectl label svc/my-service mirror.linkerd.io/exported=true + +# Verify cross-cluster connectivity +linkerd multicluster check +linkerd multicluster gateways +``` + +## Monitoring Commands + +```bash +# Live traffic view +linkerd viz top deploy/my-app + +# Per-route metrics +linkerd viz routes deploy/my-app + +# Check proxy status +linkerd viz stat deploy -n my-namespace + +# View service dependencies +linkerd viz edges deploy -n my-namespace + +# Dashboard +linkerd viz dashboard +``` + +## Debugging + +```bash +# Check injection status +linkerd check --proxy -n my-namespace + +# View proxy logs +kubectl logs deploy/my-app -c linkerd-proxy + +# Debug identity/TLS +linkerd identity -n my-namespace + +# Tap traffic (live) +linkerd viz tap deploy/my-app --to deploy/my-backend +``` + +## Best Practices + +### Do's +- **Enable mTLS everywhere** - It's automatic with Linkerd +- **Use ServiceProfiles** - Get per-route metrics and retries +- **Set retry budgets** - Prevent retry storms +- **Monitor golden metrics** - Success rate, latency, throughput + +### Don'ts +- **Don't skip check** - Always run `linkerd check` after changes +- **Don't over-configure** - Linkerd defaults are sensible +- **Don't ignore ServiceProfiles** - They unlock advanced features +- **Don't forget timeouts** - Set appropriate values per route + +## Resources + +- [Linkerd Documentation](https://linkerd.io/2.14/overview/) +- [Service Profiles](https://linkerd.io/2.14/features/service-profiles/) +- [Authorization Policy](https://linkerd.io/2.14/features/server-policy/) diff --git a/plugins/cloud-infrastructure/skills/mtls-configuration/SKILL.md b/plugins/cloud-infrastructure/skills/mtls-configuration/SKILL.md new file mode 100644 index 0000000..376914a --- /dev/null +++ b/plugins/cloud-infrastructure/skills/mtls-configuration/SKILL.md @@ -0,0 +1,347 @@ +--- +name: mtls-configuration +description: Configure mutual TLS (mTLS) for zero-trust service-to-service communication. Use when implementing zero-trust networking, certificate management, or securing internal service communication. +--- + +# mTLS Configuration + +Comprehensive guide to implementing mutual TLS for zero-trust service mesh communication. + +## When to Use This Skill + +- Implementing zero-trust networking +- Securing service-to-service communication +- Certificate rotation and management +- Debugging TLS handshake issues +- Compliance requirements (PCI-DSS, HIPAA) +- Multi-cluster secure communication + +## Core Concepts + +### 1. mTLS Flow + +``` +┌─────────┐ ┌─────────┐ +│ Service │ │ Service │ +│ A │ │ B │ +└────┬────┘ └────┬────┘ + │ │ +┌────┴────┐ TLS Handshake ┌────┴────┐ +│ Proxy │◄───────────────────────────►│ Proxy │ +│(Sidecar)│ 1. ClientHello │(Sidecar)│ +│ │ 2. ServerHello + Cert │ │ +│ │ 3. Client Cert │ │ +│ │ 4. Verify Both Certs │ │ +│ │ 5. Encrypted Channel │ │ +└─────────┘ └─────────┘ +``` + +### 2. Certificate Hierarchy + +``` +Root CA (Self-signed, long-lived) + │ + ├── Intermediate CA (Cluster-level) + │ │ + │ ├── Workload Cert (Service A) + │ └── Workload Cert (Service B) + │ + └── Intermediate CA (Multi-cluster) + │ + └── Cross-cluster certs +``` + +## Templates + +### Template 1: Istio mTLS (Strict Mode) + +```yaml +# Enable strict mTLS mesh-wide +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: istio-system +spec: + mtls: + mode: STRICT +--- +# Namespace-level override (permissive for migration) +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: legacy-namespace +spec: + mtls: + mode: PERMISSIVE +--- +# Workload-specific policy +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: payment-service + namespace: production +spec: + selector: + matchLabels: + app: payment-service + mtls: + mode: STRICT + portLevelMtls: + 8080: + mode: STRICT + 9090: + mode: DISABLE # Metrics port, no mTLS +``` + +### Template 2: Istio Destination Rule for mTLS + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: default + namespace: istio-system +spec: + host: "*.local" + trafficPolicy: + tls: + mode: ISTIO_MUTUAL +--- +# TLS to external service +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: external-api +spec: + host: api.external.com + trafficPolicy: + tls: + mode: SIMPLE + caCertificates: /etc/certs/external-ca.pem +--- +# Mutual TLS to external service +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: partner-api +spec: + host: api.partner.com + trafficPolicy: + tls: + mode: MUTUAL + clientCertificate: /etc/certs/client.pem + privateKey: /etc/certs/client-key.pem + caCertificates: /etc/certs/partner-ca.pem +``` + +### Template 3: Cert-Manager with Istio + +```yaml +# Install cert-manager issuer for Istio +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: istio-ca +spec: + ca: + secretName: istio-ca-secret +--- +# Create Istio CA secret +apiVersion: v1 +kind: Secret +metadata: + name: istio-ca-secret + namespace: cert-manager +type: kubernetes.io/tls +data: + tls.crt: + tls.key: +--- +# Certificate for workload +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: my-service-cert + namespace: my-namespace +spec: + secretName: my-service-tls + duration: 24h + renewBefore: 8h + issuerRef: + name: istio-ca + kind: ClusterIssuer + commonName: my-service.my-namespace.svc.cluster.local + dnsNames: + - my-service + - my-service.my-namespace + - my-service.my-namespace.svc + - my-service.my-namespace.svc.cluster.local + usages: + - server auth + - client auth +``` + +### Template 4: SPIFFE/SPIRE Integration + +```yaml +# SPIRE Server configuration +apiVersion: v1 +kind: ConfigMap +metadata: + name: spire-server + namespace: spire +data: + server.conf: | + server { + bind_address = "0.0.0.0" + bind_port = "8081" + trust_domain = "example.org" + data_dir = "/run/spire/data" + log_level = "INFO" + ca_ttl = "168h" + default_x509_svid_ttl = "1h" + } + + plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/run/spire/data/datastore.sqlite3" + } + } + + NodeAttestor "k8s_psat" { + plugin_data { + clusters = { + "demo-cluster" = { + service_account_allow_list = ["spire:spire-agent"] + } + } + } + } + + KeyManager "memory" { + plugin_data {} + } + + UpstreamAuthority "disk" { + plugin_data { + key_file_path = "/run/spire/secrets/bootstrap.key" + cert_file_path = "/run/spire/secrets/bootstrap.crt" + } + } + } +--- +# SPIRE Agent DaemonSet (abbreviated) +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: spire-agent + namespace: spire +spec: + selector: + matchLabels: + app: spire-agent + template: + spec: + containers: + - name: spire-agent + image: ghcr.io/spiffe/spire-agent:1.8.0 + volumeMounts: + - name: spire-agent-socket + mountPath: /run/spire/sockets + volumes: + - name: spire-agent-socket + hostPath: + path: /run/spire/sockets + type: DirectoryOrCreate +``` + +### Template 5: Linkerd mTLS (Automatic) + +```yaml +# Linkerd enables mTLS automatically +# Verify with: +# linkerd viz edges deployment -n my-namespace + +# For external services without mTLS +apiVersion: policy.linkerd.io/v1beta1 +kind: Server +metadata: + name: external-api + namespace: my-namespace +spec: + podSelector: + matchLabels: + app: my-app + port: external-api + proxyProtocol: HTTP/1 # or TLS for passthrough +--- +# Skip TLS for specific port +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + config.linkerd.io/skip-outbound-ports: "3306" # MySQL +``` + +## Certificate Rotation + +```bash +# Istio - Check certificate expiry +istioctl proxy-config secret deploy/my-app -o json | \ + jq '.dynamicActiveSecrets[0].secret.tlsCertificate.certificateChain.inlineBytes' | \ + tr -d '"' | base64 -d | openssl x509 -text -noout + +# Force certificate rotation +kubectl rollout restart deployment/my-app + +# Check Linkerd identity +linkerd identity -n my-namespace +``` + +## Debugging mTLS Issues + +```bash +# Istio - Check if mTLS is enabled +istioctl authn tls-check my-service.my-namespace.svc.cluster.local + +# Verify peer authentication +kubectl get peerauthentication --all-namespaces + +# Check destination rules +kubectl get destinationrule --all-namespaces + +# Debug TLS handshake +istioctl proxy-config log deploy/my-app --level debug +kubectl logs deploy/my-app -c istio-proxy | grep -i tls + +# Linkerd - Check mTLS status +linkerd viz edges deployment -n my-namespace +linkerd viz tap deploy/my-app --to deploy/my-backend +``` + +## Best Practices + +### Do's +- **Start with PERMISSIVE** - Migrate gradually to STRICT +- **Monitor certificate expiry** - Set up alerts +- **Use short-lived certs** - 24h or less for workloads +- **Rotate CA periodically** - Plan for CA rotation +- **Log TLS errors** - For debugging and audit + +### Don'ts +- **Don't disable mTLS** - For convenience in production +- **Don't ignore cert expiry** - Automate rotation +- **Don't use self-signed certs** - Use proper CA hierarchy +- **Don't skip verification** - Verify the full chain + +## Resources + +- [Istio Security](https://istio.io/latest/docs/concepts/security/) +- [SPIFFE/SPIRE](https://spiffe.io/) +- [cert-manager](https://cert-manager.io/) +- [Zero Trust Architecture (NIST)](https://www.nist.gov/publications/zero-trust-architecture) diff --git a/plugins/cloud-infrastructure/skills/service-mesh-observability/SKILL.md b/plugins/cloud-infrastructure/skills/service-mesh-observability/SKILL.md new file mode 100644 index 0000000..252de4c --- /dev/null +++ b/plugins/cloud-infrastructure/skills/service-mesh-observability/SKILL.md @@ -0,0 +1,383 @@ +--- +name: service-mesh-observability +description: Implement comprehensive observability for service meshes including distributed tracing, metrics, and visualization. Use when setting up mesh monitoring, debugging latency issues, or implementing SLOs for service communication. +--- + +# Service Mesh Observability + +Complete guide to observability patterns for Istio, Linkerd, and service mesh deployments. + +## When to Use This Skill + +- Setting up distributed tracing across services +- Implementing service mesh metrics and dashboards +- Debugging latency and error issues +- Defining SLOs for service communication +- Visualizing service dependencies +- Troubleshooting mesh connectivity + +## Core Concepts + +### 1. Three Pillars of Observability + +``` +┌─────────────────────────────────────────────────────┐ +│ Observability │ +├─────────────────┬─────────────────┬─────────────────┤ +│ Metrics │ Traces │ Logs │ +│ │ │ │ +│ • Request rate │ • Span context │ • Access logs │ +│ • Error rate │ • Latency │ • Error details │ +│ • Latency P50 │ • Dependencies │ • Debug info │ +│ • Saturation │ • Bottlenecks │ • Audit trail │ +└─────────────────┴─────────────────┴─────────────────┘ +``` + +### 2. Golden Signals for Mesh + +| Signal | Description | Alert Threshold | +|--------|-------------|-----------------| +| **Latency** | Request duration P50, P99 | P99 > 500ms | +| **Traffic** | Requests per second | Anomaly detection | +| **Errors** | 5xx error rate | > 1% | +| **Saturation** | Resource utilization | > 80% | + +## Templates + +### Template 1: Istio with Prometheus & Grafana + +```yaml +# Install Prometheus +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus + namespace: istio-system +data: + prometheus.yml: | + global: + scrape_interval: 15s + scrape_configs: + - job_name: 'istio-mesh' + kubernetes_sd_configs: + - role: endpoints + namespaces: + names: + - istio-system + relabel_configs: + - source_labels: [__meta_kubernetes_service_name] + action: keep + regex: istio-telemetry +--- +# ServiceMonitor for Prometheus Operator +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: istio-mesh + namespace: istio-system +spec: + selector: + matchLabels: + app: istiod + endpoints: + - port: http-monitoring + interval: 15s +``` + +### Template 2: Key Istio Metrics Queries + +```promql +# Request rate by service +sum(rate(istio_requests_total{reporter="destination"}[5m])) by (destination_service_name) + +# Error rate (5xx) +sum(rate(istio_requests_total{reporter="destination", response_code=~"5.."}[5m])) + / sum(rate(istio_requests_total{reporter="destination"}[5m])) * 100 + +# P99 latency +histogram_quantile(0.99, + sum(rate(istio_request_duration_milliseconds_bucket{reporter="destination"}[5m])) + by (le, destination_service_name)) + +# TCP connections +sum(istio_tcp_connections_opened_total{reporter="destination"}) by (destination_service_name) + +# Request size +histogram_quantile(0.99, + sum(rate(istio_request_bytes_bucket{reporter="destination"}[5m])) + by (le, destination_service_name)) +``` + +### Template 3: Jaeger Distributed Tracing + +```yaml +# Jaeger installation for Istio +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + meshConfig: + enableTracing: true + defaultConfig: + tracing: + sampling: 100.0 # 100% in dev, lower in prod + zipkin: + address: jaeger-collector.istio-system:9411 +--- +# Jaeger deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jaeger + namespace: istio-system +spec: + selector: + matchLabels: + app: jaeger + template: + metadata: + labels: + app: jaeger + spec: + containers: + - name: jaeger + image: jaegertracing/all-in-one:1.50 + ports: + - containerPort: 5775 # UDP + - containerPort: 6831 # Thrift + - containerPort: 6832 # Thrift + - containerPort: 5778 # Config + - containerPort: 16686 # UI + - containerPort: 14268 # HTTP + - containerPort: 14250 # gRPC + - containerPort: 9411 # Zipkin + env: + - name: COLLECTOR_ZIPKIN_HOST_PORT + value: ":9411" +``` + +### Template 4: Linkerd Viz Dashboard + +```bash +# Install Linkerd viz extension +linkerd viz install | kubectl apply -f - + +# Access dashboard +linkerd viz dashboard + +# CLI commands for observability +# Top requests +linkerd viz top deploy/my-app + +# Per-route metrics +linkerd viz routes deploy/my-app --to deploy/backend + +# Live traffic inspection +linkerd viz tap deploy/my-app --to deploy/backend + +# Service edges (dependencies) +linkerd viz edges deployment -n my-namespace +``` + +### Template 5: Grafana Dashboard JSON + +```json +{ + "dashboard": { + "title": "Service Mesh Overview", + "panels": [ + { + "title": "Request Rate", + "type": "graph", + "targets": [ + { + "expr": "sum(rate(istio_requests_total{reporter=\"destination\"}[5m])) by (destination_service_name)", + "legendFormat": "{{destination_service_name}}" + } + ] + }, + { + "title": "Error Rate", + "type": "gauge", + "targets": [ + { + "expr": "sum(rate(istio_requests_total{response_code=~\"5..\"}[5m])) / sum(rate(istio_requests_total[5m])) * 100" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "steps": [ + {"value": 0, "color": "green"}, + {"value": 1, "color": "yellow"}, + {"value": 5, "color": "red"} + ] + } + } + } + }, + { + "title": "P99 Latency", + "type": "graph", + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(istio_request_duration_milliseconds_bucket{reporter=\"destination\"}[5m])) by (le, destination_service_name))", + "legendFormat": "{{destination_service_name}}" + } + ] + }, + { + "title": "Service Topology", + "type": "nodeGraph", + "targets": [ + { + "expr": "sum(rate(istio_requests_total{reporter=\"destination\"}[5m])) by (source_workload, destination_service_name)" + } + ] + } + ] + } +} +``` + +### Template 6: Kiali Service Mesh Visualization + +```yaml +# Kiali installation +apiVersion: kiali.io/v1alpha1 +kind: Kiali +metadata: + name: kiali + namespace: istio-system +spec: + auth: + strategy: anonymous # or openid, token + deployment: + accessible_namespaces: + - "**" + external_services: + prometheus: + url: http://prometheus.istio-system:9090 + tracing: + url: http://jaeger-query.istio-system:16686 + grafana: + url: http://grafana.istio-system:3000 +``` + +### Template 7: OpenTelemetry Integration + +```yaml +# OpenTelemetry Collector for mesh +apiVersion: v1 +kind: ConfigMap +metadata: + name: otel-collector-config +data: + config.yaml: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + zipkin: + endpoint: 0.0.0.0:9411 + + processors: + batch: + timeout: 10s + + exporters: + jaeger: + endpoint: jaeger-collector:14250 + tls: + insecure: true + prometheus: + endpoint: 0.0.0.0:8889 + + service: + pipelines: + traces: + receivers: [otlp, zipkin] + processors: [batch] + exporters: [jaeger] + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] +--- +# Istio Telemetry v2 with OTel +apiVersion: telemetry.istio.io/v1alpha1 +kind: Telemetry +metadata: + name: mesh-default + namespace: istio-system +spec: + tracing: + - providers: + - name: otel + randomSamplingPercentage: 10 +``` + +## Alerting Rules + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: mesh-alerts + namespace: istio-system +spec: + groups: + - name: mesh.rules + rules: + - alert: HighErrorRate + expr: | + sum(rate(istio_requests_total{response_code=~"5.."}[5m])) by (destination_service_name) + / sum(rate(istio_requests_total[5m])) by (destination_service_name) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "High error rate for {{ $labels.destination_service_name }}" + + - alert: HighLatency + expr: | + histogram_quantile(0.99, sum(rate(istio_request_duration_milliseconds_bucket[5m])) + by (le, destination_service_name)) > 1000 + for: 5m + labels: + severity: warning + annotations: + summary: "High P99 latency for {{ $labels.destination_service_name }}" + + - alert: MeshCertExpiring + expr: | + (certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 7 + labels: + severity: warning + annotations: + summary: "Mesh certificate expiring in less than 7 days" +``` + +## Best Practices + +### Do's +- **Sample appropriately** - 100% in dev, 1-10% in prod +- **Use trace context** - Propagate headers consistently +- **Set up alerts** - For golden signals +- **Correlate metrics/traces** - Use exemplars +- **Retain strategically** - Hot/cold storage tiers + +### Don'ts +- **Don't over-sample** - Storage costs add up +- **Don't ignore cardinality** - Limit label values +- **Don't skip dashboards** - Visualize dependencies +- **Don't forget costs** - Monitor observability costs + +## Resources + +- [Istio Observability](https://istio.io/latest/docs/tasks/observability/) +- [Linkerd Observability](https://linkerd.io/2.14/features/dashboard/) +- [OpenTelemetry](https://opentelemetry.io/) +- [Kiali](https://kiali.io/) diff --git a/plugins/data-engineering/skills/airflow-dag-patterns/SKILL.md b/plugins/data-engineering/skills/airflow-dag-patterns/SKILL.md new file mode 100644 index 0000000..e278c1f --- /dev/null +++ b/plugins/data-engineering/skills/airflow-dag-patterns/SKILL.md @@ -0,0 +1,523 @@ +--- +name: airflow-dag-patterns +description: Build production Apache Airflow DAGs with best practices for operators, sensors, testing, and deployment. Use when creating data pipelines, orchestrating workflows, or scheduling batch jobs. +--- + +# Apache Airflow DAG Patterns + +Production-ready patterns for Apache Airflow including DAG design, operators, sensors, testing, and deployment strategies. + +## When to Use This Skill + +- Creating data pipeline orchestration with Airflow +- Designing DAG structures and dependencies +- Implementing custom operators and sensors +- Testing Airflow DAGs locally +- Setting up Airflow in production +- Debugging failed DAG runs + +## Core Concepts + +### 1. DAG Design Principles + +| Principle | Description | +|-----------|-------------| +| **Idempotent** | Running twice produces same result | +| **Atomic** | Tasks succeed or fail completely | +| **Incremental** | Process only new/changed data | +| **Observable** | Logs, metrics, alerts at every step | + +### 2. Task Dependencies + +```python +# Linear +task1 >> task2 >> task3 + +# Fan-out +task1 >> [task2, task3, task4] + +# Fan-in +[task1, task2, task3] >> task4 + +# Complex +task1 >> task2 >> task4 +task1 >> task3 >> task4 +``` + +## Quick Start + +```python +# dags/example_dag.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.operators.empty import EmptyOperator + +default_args = { + 'owner': 'data-team', + 'depends_on_past': False, + 'email_on_failure': True, + 'email_on_retry': False, + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + 'retry_exponential_backoff': True, + 'max_retry_delay': timedelta(hours=1), +} + +with DAG( + dag_id='example_etl', + default_args=default_args, + description='Example ETL pipeline', + schedule='0 6 * * *', # Daily at 6 AM + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'example'], + max_active_runs=1, +) as dag: + + start = EmptyOperator(task_id='start') + + def extract_data(**context): + execution_date = context['ds'] + # Extract logic here + return {'records': 1000} + + extract = PythonOperator( + task_id='extract', + python_callable=extract_data, + ) + + end = EmptyOperator(task_id='end') + + start >> extract >> end +``` + +## Patterns + +### Pattern 1: TaskFlow API (Airflow 2.0+) + +```python +# dags/taskflow_example.py +from datetime import datetime +from airflow.decorators import dag, task +from airflow.models import Variable + +@dag( + dag_id='taskflow_etl', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'taskflow'], +) +def taskflow_etl(): + """ETL pipeline using TaskFlow API""" + + @task() + def extract(source: str) -> dict: + """Extract data from source""" + import pandas as pd + + df = pd.read_csv(f's3://bucket/{source}/{{ ds }}.csv') + return {'data': df.to_dict(), 'rows': len(df)} + + @task() + def transform(extracted: dict) -> dict: + """Transform extracted data""" + import pandas as pd + + df = pd.DataFrame(extracted['data']) + df['processed_at'] = datetime.now() + df = df.dropna() + return {'data': df.to_dict(), 'rows': len(df)} + + @task() + def load(transformed: dict, target: str): + """Load data to target""" + import pandas as pd + + df = pd.DataFrame(transformed['data']) + df.to_parquet(f's3://bucket/{target}/{{ ds }}.parquet') + return transformed['rows'] + + @task() + def notify(rows_loaded: int): + """Send notification""" + print(f'Loaded {rows_loaded} rows') + + # Define dependencies with XCom passing + extracted = extract(source='raw_data') + transformed = transform(extracted) + loaded = load(transformed, target='processed_data') + notify(loaded) + +# Instantiate the DAG +taskflow_etl() +``` + +### Pattern 2: Dynamic DAG Generation + +```python +# dags/dynamic_dag_factory.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.models import Variable +import json + +# Configuration for multiple similar pipelines +PIPELINE_CONFIGS = [ + {'name': 'customers', 'schedule': '@daily', 'source': 's3://raw/customers'}, + {'name': 'orders', 'schedule': '@hourly', 'source': 's3://raw/orders'}, + {'name': 'products', 'schedule': '@weekly', 'source': 's3://raw/products'}, +] + +def create_dag(config: dict) -> DAG: + """Factory function to create DAGs from config""" + + dag_id = f"etl_{config['name']}" + + default_args = { + 'owner': 'data-team', + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + } + + dag = DAG( + dag_id=dag_id, + default_args=default_args, + schedule=config['schedule'], + start_date=datetime(2024, 1, 1), + catchup=False, + tags=['etl', 'dynamic', config['name']], + ) + + with dag: + def extract_fn(source, **context): + print(f"Extracting from {source} for {context['ds']}") + + def transform_fn(**context): + print(f"Transforming data for {context['ds']}") + + def load_fn(table_name, **context): + print(f"Loading to {table_name} for {context['ds']}") + + extract = PythonOperator( + task_id='extract', + python_callable=extract_fn, + op_kwargs={'source': config['source']}, + ) + + transform = PythonOperator( + task_id='transform', + python_callable=transform_fn, + ) + + load = PythonOperator( + task_id='load', + python_callable=load_fn, + op_kwargs={'table_name': config['name']}, + ) + + extract >> transform >> load + + return dag + +# Generate DAGs +for config in PIPELINE_CONFIGS: + globals()[f"dag_{config['name']}"] = create_dag(config) +``` + +### Pattern 3: Branching and Conditional Logic + +```python +# dags/branching_example.py +from airflow.decorators import dag, task +from airflow.operators.python import BranchPythonOperator +from airflow.operators.empty import EmptyOperator +from airflow.utils.trigger_rule import TriggerRule + +@dag( + dag_id='branching_pipeline', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, +) +def branching_pipeline(): + + @task() + def check_data_quality() -> dict: + """Check data quality and return metrics""" + quality_score = 0.95 # Simulated + return {'score': quality_score, 'rows': 10000} + + def choose_branch(**context) -> str: + """Determine which branch to execute""" + ti = context['ti'] + metrics = ti.xcom_pull(task_ids='check_data_quality') + + if metrics['score'] >= 0.9: + return 'high_quality_path' + elif metrics['score'] >= 0.7: + return 'medium_quality_path' + else: + return 'low_quality_path' + + quality_check = check_data_quality() + + branch = BranchPythonOperator( + task_id='branch', + python_callable=choose_branch, + ) + + high_quality = EmptyOperator(task_id='high_quality_path') + medium_quality = EmptyOperator(task_id='medium_quality_path') + low_quality = EmptyOperator(task_id='low_quality_path') + + # Join point - runs after any branch completes + join = EmptyOperator( + task_id='join', + trigger_rule=TriggerRule.NONE_FAILED_MIN_ONE_SUCCESS, + ) + + quality_check >> branch >> [high_quality, medium_quality, low_quality] >> join + +branching_pipeline() +``` + +### Pattern 4: Sensors and External Dependencies + +```python +# dags/sensor_patterns.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.sensors.filesystem import FileSensor +from airflow.providers.amazon.aws.sensors.s3 import S3KeySensor +from airflow.sensors.external_task import ExternalTaskSensor +from airflow.operators.python import PythonOperator + +with DAG( + dag_id='sensor_example', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, +) as dag: + + # Wait for file on S3 + wait_for_file = S3KeySensor( + task_id='wait_for_s3_file', + bucket_name='data-lake', + bucket_key='raw/{{ ds }}/data.parquet', + aws_conn_id='aws_default', + timeout=60 * 60 * 2, # 2 hours + poke_interval=60 * 5, # Check every 5 minutes + mode='reschedule', # Free up worker slot while waiting + ) + + # Wait for another DAG to complete + wait_for_upstream = ExternalTaskSensor( + task_id='wait_for_upstream_dag', + external_dag_id='upstream_etl', + external_task_id='final_task', + execution_date_fn=lambda dt: dt, # Same execution date + timeout=60 * 60 * 3, + mode='reschedule', + ) + + # Custom sensor using @task.sensor decorator + @task.sensor(poke_interval=60, timeout=3600, mode='reschedule') + def wait_for_api() -> PokeReturnValue: + """Custom sensor for API availability""" + import requests + + response = requests.get('https://api.example.com/health') + is_done = response.status_code == 200 + + return PokeReturnValue(is_done=is_done, xcom_value=response.json()) + + api_ready = wait_for_api() + + def process_data(**context): + api_result = context['ti'].xcom_pull(task_ids='wait_for_api') + print(f"API returned: {api_result}") + + process = PythonOperator( + task_id='process', + python_callable=process_data, + ) + + [wait_for_file, wait_for_upstream, api_ready] >> process +``` + +### Pattern 5: Error Handling and Alerts + +```python +# dags/error_handling.py +from datetime import datetime, timedelta +from airflow import DAG +from airflow.operators.python import PythonOperator +from airflow.utils.trigger_rule import TriggerRule +from airflow.models import Variable + +def task_failure_callback(context): + """Callback on task failure""" + task_instance = context['task_instance'] + exception = context.get('exception') + + # Send to Slack/PagerDuty/etc + message = f""" + Task Failed! + DAG: {task_instance.dag_id} + Task: {task_instance.task_id} + Execution Date: {context['ds']} + Error: {exception} + Log URL: {task_instance.log_url} + """ + # send_slack_alert(message) + print(message) + +def dag_failure_callback(context): + """Callback on DAG failure""" + # Aggregate failures, send summary + pass + +with DAG( + dag_id='error_handling_example', + schedule='@daily', + start_date=datetime(2024, 1, 1), + catchup=False, + on_failure_callback=dag_failure_callback, + default_args={ + 'on_failure_callback': task_failure_callback, + 'retries': 3, + 'retry_delay': timedelta(minutes=5), + }, +) as dag: + + def might_fail(**context): + import random + if random.random() < 0.3: + raise ValueError("Random failure!") + return "Success" + + risky_task = PythonOperator( + task_id='risky_task', + python_callable=might_fail, + ) + + def cleanup(**context): + """Cleanup runs regardless of upstream failures""" + print("Cleaning up...") + + cleanup_task = PythonOperator( + task_id='cleanup', + python_callable=cleanup, + trigger_rule=TriggerRule.ALL_DONE, # Run even if upstream fails + ) + + def notify_success(**context): + """Only runs if all upstream succeeded""" + print("All tasks succeeded!") + + success_notification = PythonOperator( + task_id='notify_success', + python_callable=notify_success, + trigger_rule=TriggerRule.ALL_SUCCESS, + ) + + risky_task >> [cleanup_task, success_notification] +``` + +### Pattern 6: Testing DAGs + +```python +# tests/test_dags.py +import pytest +from datetime import datetime +from airflow.models import DagBag + +@pytest.fixture +def dagbag(): + return DagBag(dag_folder='dags/', include_examples=False) + +def test_dag_loaded(dagbag): + """Test that all DAGs load without errors""" + assert len(dagbag.import_errors) == 0, f"DAG import errors: {dagbag.import_errors}" + +def test_dag_structure(dagbag): + """Test specific DAG structure""" + dag = dagbag.get_dag('example_etl') + + assert dag is not None + assert len(dag.tasks) == 3 + assert dag.schedule_interval == '0 6 * * *' + +def test_task_dependencies(dagbag): + """Test task dependencies are correct""" + dag = dagbag.get_dag('example_etl') + + extract_task = dag.get_task('extract') + assert 'start' in [t.task_id for t in extract_task.upstream_list] + assert 'end' in [t.task_id for t in extract_task.downstream_list] + +def test_dag_integrity(dagbag): + """Test DAG has no cycles and is valid""" + for dag_id, dag in dagbag.dags.items(): + assert dag.test_cycle() is None, f"Cycle detected in {dag_id}" + +# Test individual task logic +def test_extract_function(): + """Unit test for extract function""" + from dags.example_dag import extract_data + + result = extract_data(ds='2024-01-01') + assert 'records' in result + assert isinstance(result['records'], int) +``` + +## Project Structure + +``` +airflow/ +├── dags/ +│ ├── __init__.py +│ ├── common/ +│ │ ├── __init__.py +│ │ ├── operators.py # Custom operators +│ │ ├── sensors.py # Custom sensors +│ │ └── callbacks.py # Alert callbacks +│ ├── etl/ +│ │ ├── customers.py +│ │ └── orders.py +│ └── ml/ +│ └── training.py +├── plugins/ +│ └── custom_plugin.py +├── tests/ +│ ├── __init__.py +│ ├── test_dags.py +│ └── test_operators.py +├── docker-compose.yml +└── requirements.txt +``` + +## Best Practices + +### Do's +- **Use TaskFlow API** - Cleaner code, automatic XCom +- **Set timeouts** - Prevent zombie tasks +- **Use `mode='reschedule'`** - For sensors, free up workers +- **Test DAGs** - Unit tests and integration tests +- **Idempotent tasks** - Safe to retry + +### Don'ts +- **Don't use `depends_on_past=True`** - Creates bottlenecks +- **Don't hardcode dates** - Use `{{ ds }}` macros +- **Don't use global state** - Tasks should be stateless +- **Don't skip catchup blindly** - Understand implications +- **Don't put heavy logic in DAG file** - Import from modules + +## Resources + +- [Airflow Documentation](https://airflow.apache.org/docs/) +- [Astronomer Guides](https://docs.astronomer.io/learn) +- [TaskFlow API](https://airflow.apache.org/docs/apache-airflow/stable/tutorial/taskflow.html) diff --git a/plugins/data-engineering/skills/data-quality-frameworks/SKILL.md b/plugins/data-engineering/skills/data-quality-frameworks/SKILL.md new file mode 100644 index 0000000..711466f --- /dev/null +++ b/plugins/data-engineering/skills/data-quality-frameworks/SKILL.md @@ -0,0 +1,587 @@ +--- +name: data-quality-frameworks +description: Implement data quality validation with Great Expectations, dbt tests, and data contracts. Use when building data quality pipelines, implementing validation rules, or establishing data contracts. +--- + +# Data Quality Frameworks + +Production patterns for implementing data quality with Great Expectations, dbt tests, and data contracts to ensure reliable data pipelines. + +## When to Use This Skill + +- Implementing data quality checks in pipelines +- Setting up Great Expectations validation +- Building comprehensive dbt test suites +- Establishing data contracts between teams +- Monitoring data quality metrics +- Automating data validation in CI/CD + +## Core Concepts + +### 1. Data Quality Dimensions + +| Dimension | Description | Example Check | +|-----------|-------------|---------------| +| **Completeness** | No missing values | `expect_column_values_to_not_be_null` | +| **Uniqueness** | No duplicates | `expect_column_values_to_be_unique` | +| **Validity** | Values in expected range | `expect_column_values_to_be_in_set` | +| **Accuracy** | Data matches reality | Cross-reference validation | +| **Consistency** | No contradictions | `expect_column_pair_values_A_to_be_greater_than_B` | +| **Timeliness** | Data is recent | `expect_column_max_to_be_between` | + +### 2. Testing Pyramid for Data + +``` + /\ + / \ Integration Tests (cross-table) + /────\ + / \ Unit Tests (single column) + /────────\ + / \ Schema Tests (structure) + /────────────\ +``` + +## Quick Start + +### Great Expectations Setup + +```bash +# Install +pip install great_expectations + +# Initialize project +great_expectations init + +# Create datasource +great_expectations datasource new +``` + +```python +# great_expectations/checkpoints/daily_validation.yml +import great_expectations as gx + +# Create context +context = gx.get_context() + +# Create expectation suite +suite = context.add_expectation_suite("orders_suite") + +# Add expectations +suite.add_expectation( + gx.expectations.ExpectColumnValuesToNotBeNull(column="order_id") +) +suite.add_expectation( + gx.expectations.ExpectColumnValuesToBeUnique(column="order_id") +) + +# Validate +results = context.run_checkpoint(checkpoint_name="daily_orders") +``` + +## Patterns + +### Pattern 1: Great Expectations Suite + +```python +# expectations/orders_suite.py +import great_expectations as gx +from great_expectations.core import ExpectationSuite +from great_expectations.core.expectation_configuration import ExpectationConfiguration + +def build_orders_suite() -> ExpectationSuite: + """Build comprehensive orders expectation suite""" + + suite = ExpectationSuite(expectation_suite_name="orders_suite") + + # Schema expectations + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_table_columns_to_match_set", + kwargs={ + "column_set": ["order_id", "customer_id", "amount", "status", "created_at"], + "exact_match": False # Allow additional columns + } + )) + + # Primary key + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_not_be_null", + kwargs={"column": "order_id"} + )) + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_be_unique", + kwargs={"column": "order_id"} + )) + + # Foreign key + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_not_be_null", + kwargs={"column": "customer_id"} + )) + + # Categorical values + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_be_in_set", + kwargs={ + "column": "status", + "value_set": ["pending", "processing", "shipped", "delivered", "cancelled"] + } + )) + + # Numeric ranges + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_be_between", + kwargs={ + "column": "amount", + "min_value": 0, + "max_value": 100000, + "strict_min": True # amount > 0 + } + )) + + # Date validity + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_values_to_be_dateutil_parseable", + kwargs={"column": "created_at"} + )) + + # Freshness - data should be recent + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_max_to_be_between", + kwargs={ + "column": "created_at", + "min_value": {"$PARAMETER": "now - timedelta(days=1)"}, + "max_value": {"$PARAMETER": "now"} + } + )) + + # Row count sanity + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_table_row_count_to_be_between", + kwargs={ + "min_value": 1000, # Expect at least 1000 rows + "max_value": 10000000 + } + )) + + # Statistical expectations + suite.add_expectation(ExpectationConfiguration( + expectation_type="expect_column_mean_to_be_between", + kwargs={ + "column": "amount", + "min_value": 50, + "max_value": 500 + } + )) + + return suite +``` + +### Pattern 2: Great Expectations Checkpoint + +```yaml +# great_expectations/checkpoints/orders_checkpoint.yml +name: orders_checkpoint +config_version: 1.0 +class_name: Checkpoint +run_name_template: "%Y%m%d-%H%M%S-orders-validation" + +validations: + - batch_request: + datasource_name: warehouse + data_connector_name: default_inferred_data_connector_name + data_asset_name: orders + data_connector_query: + index: -1 # Latest batch + expectation_suite_name: orders_suite + +action_list: + - name: store_validation_result + action: + class_name: StoreValidationResultAction + + - name: store_evaluation_parameters + action: + class_name: StoreEvaluationParametersAction + + - name: update_data_docs + action: + class_name: UpdateDataDocsAction + + # Slack notification on failure + - name: send_slack_notification + action: + class_name: SlackNotificationAction + slack_webhook: ${SLACK_WEBHOOK} + notify_on: failure + renderer: + module_name: great_expectations.render.renderer.slack_renderer + class_name: SlackRenderer +``` + +```python +# Run checkpoint +import great_expectations as gx + +context = gx.get_context() +result = context.run_checkpoint(checkpoint_name="orders_checkpoint") + +if not result.success: + failed_expectations = [ + r for r in result.run_results.values() + if not r.success + ] + raise ValueError(f"Data quality check failed: {failed_expectations}") +``` + +### Pattern 3: dbt Data Tests + +```yaml +# models/marts/core/_core__models.yml +version: 2 + +models: + - name: fct_orders + description: Order fact table + tests: + # Table-level tests + - dbt_utils.recency: + datepart: day + field: created_at + interval: 1 + - dbt_utils.at_least_one + - dbt_utils.expression_is_true: + expression: "total_amount >= 0" + + columns: + - name: order_id + description: Primary key + tests: + - unique + - not_null + + - name: customer_id + description: Foreign key to dim_customers + tests: + - not_null + - relationships: + to: ref('dim_customers') + field: customer_id + + - name: order_status + tests: + - accepted_values: + values: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'] + + - name: total_amount + tests: + - not_null + - dbt_utils.expression_is_true: + expression: ">= 0" + + - name: created_at + tests: + - not_null + - dbt_utils.expression_is_true: + expression: "<= current_timestamp" + + - name: dim_customers + columns: + - name: customer_id + tests: + - unique + - not_null + + - name: email + tests: + - unique + - not_null + # Custom regex test + - dbt_utils.expression_is_true: + expression: "email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'" +``` + +### Pattern 4: Custom dbt Tests + +```sql +-- tests/generic/test_row_count_in_range.sql +{% test row_count_in_range(model, min_count, max_count) %} + +with row_count as ( + select count(*) as cnt from {{ model }} +) + +select cnt +from row_count +where cnt < {{ min_count }} or cnt > {{ max_count }} + +{% endtest %} + +-- Usage in schema.yml: +-- tests: +-- - row_count_in_range: +-- min_count: 1000 +-- max_count: 10000000 +``` + +```sql +-- tests/generic/test_sequential_values.sql +{% test sequential_values(model, column_name, interval=1) %} + +with lagged as ( + select + {{ column_name }}, + lag({{ column_name }}) over (order by {{ column_name }}) as prev_value + from {{ model }} +) + +select * +from lagged +where {{ column_name }} - prev_value != {{ interval }} + and prev_value is not null + +{% endtest %} +``` + +```sql +-- tests/singular/assert_orders_customers_match.sql +-- Singular test: specific business rule + +with orders_customers as ( + select distinct customer_id from {{ ref('fct_orders') }} +), + +dim_customers as ( + select customer_id from {{ ref('dim_customers') }} +), + +orphaned_orders as ( + select o.customer_id + from orders_customers o + left join dim_customers c using (customer_id) + where c.customer_id is null +) + +select * from orphaned_orders +-- Test passes if this returns 0 rows +``` + +### Pattern 5: Data Contracts + +```yaml +# contracts/orders_contract.yaml +apiVersion: datacontract.com/v1.0.0 +kind: DataContract +metadata: + name: orders + version: 1.0.0 + owner: data-platform-team + contact: data-team@company.com + +info: + title: Orders Data Contract + description: Contract for order event data from the ecommerce platform + purpose: Analytics, reporting, and ML features + +servers: + production: + type: snowflake + account: company.us-east-1 + database: ANALYTICS + schema: CORE + +terms: + usage: Internal analytics only + limitations: PII must not be exposed in downstream marts + billing: Charged per query TB scanned + +schema: + type: object + properties: + order_id: + type: string + format: uuid + description: Unique order identifier + required: true + unique: true + pii: false + + customer_id: + type: string + format: uuid + description: Customer identifier + required: true + pii: true + piiClassification: indirect + + total_amount: + type: number + minimum: 0 + maximum: 100000 + description: Order total in USD + + created_at: + type: string + format: date-time + description: Order creation timestamp + required: true + + status: + type: string + enum: [pending, processing, shipped, delivered, cancelled] + description: Current order status + +quality: + type: SodaCL + specification: + checks for orders: + - row_count > 0 + - missing_count(order_id) = 0 + - duplicate_count(order_id) = 0 + - invalid_count(status) = 0: + valid values: [pending, processing, shipped, delivered, cancelled] + - freshness(created_at) < 24h + +sla: + availability: 99.9% + freshness: 1 hour + latency: 5 minutes +``` + +### Pattern 6: Automated Quality Pipeline + +```python +# quality_pipeline.py +from dataclasses import dataclass +from typing import List, Dict, Any +import great_expectations as gx +from datetime import datetime + +@dataclass +class QualityResult: + table: str + passed: bool + total_expectations: int + failed_expectations: int + details: List[Dict[str, Any]] + timestamp: datetime + +class DataQualityPipeline: + """Orchestrate data quality checks across tables""" + + def __init__(self, context: gx.DataContext): + self.context = context + self.results: List[QualityResult] = [] + + def validate_table(self, table: str, suite: str) -> QualityResult: + """Validate a single table against expectation suite""" + + checkpoint_config = { + "name": f"{table}_validation", + "config_version": 1.0, + "class_name": "Checkpoint", + "validations": [{ + "batch_request": { + "datasource_name": "warehouse", + "data_asset_name": table, + }, + "expectation_suite_name": suite, + }], + } + + result = self.context.run_checkpoint(**checkpoint_config) + + # Parse results + validation_result = list(result.run_results.values())[0] + results = validation_result.results + + failed = [r for r in results if not r.success] + + return QualityResult( + table=table, + passed=result.success, + total_expectations=len(results), + failed_expectations=len(failed), + details=[{ + "expectation": r.expectation_config.expectation_type, + "success": r.success, + "observed_value": r.result.get("observed_value"), + } for r in results], + timestamp=datetime.now() + ) + + def run_all(self, tables: Dict[str, str]) -> Dict[str, QualityResult]: + """Run validation for all tables""" + results = {} + + for table, suite in tables.items(): + print(f"Validating {table}...") + results[table] = self.validate_table(table, suite) + + return results + + def generate_report(self, results: Dict[str, QualityResult]) -> str: + """Generate quality report""" + report = ["# Data Quality Report", f"Generated: {datetime.now()}", ""] + + total_passed = sum(1 for r in results.values() if r.passed) + total_tables = len(results) + + report.append(f"## Summary: {total_passed}/{total_tables} tables passed") + report.append("") + + for table, result in results.items(): + status = "✅" if result.passed else "❌" + report.append(f"### {status} {table}") + report.append(f"- Expectations: {result.total_expectations}") + report.append(f"- Failed: {result.failed_expectations}") + + if not result.passed: + report.append("- Failed checks:") + for detail in result.details: + if not detail["success"]: + report.append(f" - {detail['expectation']}: {detail['observed_value']}") + report.append("") + + return "\n".join(report) + +# Usage +context = gx.get_context() +pipeline = DataQualityPipeline(context) + +tables_to_validate = { + "orders": "orders_suite", + "customers": "customers_suite", + "products": "products_suite", +} + +results = pipeline.run_all(tables_to_validate) +report = pipeline.generate_report(results) + +# Fail pipeline if any table failed +if not all(r.passed for r in results.values()): + print(report) + raise ValueError("Data quality checks failed!") +``` + +## Best Practices + +### Do's +- **Test early** - Validate source data before transformations +- **Test incrementally** - Add tests as you find issues +- **Document expectations** - Clear descriptions for each test +- **Alert on failures** - Integrate with monitoring +- **Version contracts** - Track schema changes + +### Don'ts +- **Don't test everything** - Focus on critical columns +- **Don't ignore warnings** - They often precede failures +- **Don't skip freshness** - Stale data is bad data +- **Don't hardcode thresholds** - Use dynamic baselines +- **Don't test in isolation** - Test relationships too + +## Resources + +- [Great Expectations Documentation](https://docs.greatexpectations.io/) +- [dbt Testing Documentation](https://docs.getdbt.com/docs/build/tests) +- [Data Contract Specification](https://datacontract.com/) +- [Soda Core](https://docs.soda.io/soda-core/overview.html) diff --git a/plugins/data-engineering/skills/dbt-transformation-patterns/SKILL.md b/plugins/data-engineering/skills/dbt-transformation-patterns/SKILL.md new file mode 100644 index 0000000..8758288 --- /dev/null +++ b/plugins/data-engineering/skills/dbt-transformation-patterns/SKILL.md @@ -0,0 +1,561 @@ +--- +name: dbt-transformation-patterns +description: Master dbt (data build tool) for analytics engineering with model organization, testing, documentation, and incremental strategies. Use when building data transformations, creating data models, or implementing analytics engineering best practices. +--- + +# dbt Transformation Patterns + +Production-ready patterns for dbt (data build tool) including model organization, testing strategies, documentation, and incremental processing. + +## When to Use This Skill + +- Building data transformation pipelines with dbt +- Organizing models into staging, intermediate, and marts layers +- Implementing data quality tests +- Creating incremental models for large datasets +- Documenting data models and lineage +- Setting up dbt project structure + +## Core Concepts + +### 1. Model Layers (Medallion Architecture) + +``` +sources/ Raw data definitions + ↓ +staging/ 1:1 with source, light cleaning + ↓ +intermediate/ Business logic, joins, aggregations + ↓ +marts/ Final analytics tables +``` + +### 2. Naming Conventions + +| Layer | Prefix | Example | +|-------|--------|---------| +| Staging | `stg_` | `stg_stripe__payments` | +| Intermediate | `int_` | `int_payments_pivoted` | +| Marts | `dim_`, `fct_` | `dim_customers`, `fct_orders` | + +## Quick Start + +```yaml +# dbt_project.yml +name: 'analytics' +version: '1.0.0' +profile: 'analytics' + +model-paths: ["models"] +analysis-paths: ["analyses"] +test-paths: ["tests"] +seed-paths: ["seeds"] +macro-paths: ["macros"] + +vars: + start_date: '2020-01-01' + +models: + analytics: + staging: + +materialized: view + +schema: staging + intermediate: + +materialized: ephemeral + marts: + +materialized: table + +schema: analytics +``` + +``` +# Project structure +models/ +├── staging/ +│ ├── stripe/ +│ │ ├── _stripe__sources.yml +│ │ ├── _stripe__models.yml +│ │ ├── stg_stripe__customers.sql +│ │ └── stg_stripe__payments.sql +│ └── shopify/ +│ ├── _shopify__sources.yml +│ └── stg_shopify__orders.sql +├── intermediate/ +│ └── finance/ +│ └── int_payments_pivoted.sql +└── marts/ + ├── core/ + │ ├── _core__models.yml + │ ├── dim_customers.sql + │ └── fct_orders.sql + └── finance/ + └── fct_revenue.sql +``` + +## Patterns + +### Pattern 1: Source Definitions + +```yaml +# models/staging/stripe/_stripe__sources.yml +version: 2 + +sources: + - name: stripe + description: Raw Stripe data loaded via Fivetran + database: raw + schema: stripe + loader: fivetran + loaded_at_field: _fivetran_synced + freshness: + warn_after: {count: 12, period: hour} + error_after: {count: 24, period: hour} + tables: + - name: customers + description: Stripe customer records + columns: + - name: id + description: Primary key + tests: + - unique + - not_null + - name: email + description: Customer email + - name: created + description: Account creation timestamp + + - name: payments + description: Stripe payment transactions + columns: + - name: id + tests: + - unique + - not_null + - name: customer_id + tests: + - not_null + - relationships: + to: source('stripe', 'customers') + field: id +``` + +### Pattern 2: Staging Models + +```sql +-- models/staging/stripe/stg_stripe__customers.sql +with source as ( + select * from {{ source('stripe', 'customers') }} +), + +renamed as ( + select + -- ids + id as customer_id, + + -- strings + lower(email) as email, + name as customer_name, + + -- timestamps + created as created_at, + + -- metadata + _fivetran_synced as _loaded_at + + from source +) + +select * from renamed +``` + +```sql +-- models/staging/stripe/stg_stripe__payments.sql +{{ + config( + materialized='incremental', + unique_key='payment_id', + on_schema_change='append_new_columns' + ) +}} + +with source as ( + select * from {{ source('stripe', 'payments') }} + + {% if is_incremental() %} + where _fivetran_synced > (select max(_loaded_at) from {{ this }}) + {% endif %} +), + +renamed as ( + select + -- ids + id as payment_id, + customer_id, + invoice_id, + + -- amounts (convert cents to dollars) + amount / 100.0 as amount, + amount_refunded / 100.0 as amount_refunded, + + -- status + status as payment_status, + + -- timestamps + created as created_at, + + -- metadata + _fivetran_synced as _loaded_at + + from source +) + +select * from renamed +``` + +### Pattern 3: Intermediate Models + +```sql +-- models/intermediate/finance/int_payments_pivoted_to_customer.sql +with payments as ( + select * from {{ ref('stg_stripe__payments') }} +), + +customers as ( + select * from {{ ref('stg_stripe__customers') }} +), + +payment_summary as ( + select + customer_id, + count(*) as total_payments, + count(case when payment_status = 'succeeded' then 1 end) as successful_payments, + sum(case when payment_status = 'succeeded' then amount else 0 end) as total_amount_paid, + min(created_at) as first_payment_at, + max(created_at) as last_payment_at + from payments + group by customer_id +) + +select + customers.customer_id, + customers.email, + customers.created_at as customer_created_at, + coalesce(payment_summary.total_payments, 0) as total_payments, + coalesce(payment_summary.successful_payments, 0) as successful_payments, + coalesce(payment_summary.total_amount_paid, 0) as lifetime_value, + payment_summary.first_payment_at, + payment_summary.last_payment_at + +from customers +left join payment_summary using (customer_id) +``` + +### Pattern 4: Mart Models (Dimensions and Facts) + +```sql +-- models/marts/core/dim_customers.sql +{{ + config( + materialized='table', + unique_key='customer_id' + ) +}} + +with customers as ( + select * from {{ ref('int_payments_pivoted_to_customer') }} +), + +orders as ( + select * from {{ ref('stg_shopify__orders') }} +), + +order_summary as ( + select + customer_id, + count(*) as total_orders, + sum(total_price) as total_order_value, + min(created_at) as first_order_at, + max(created_at) as last_order_at + from orders + group by customer_id +), + +final as ( + select + -- surrogate key + {{ dbt_utils.generate_surrogate_key(['customers.customer_id']) }} as customer_key, + + -- natural key + customers.customer_id, + + -- attributes + customers.email, + customers.customer_created_at, + + -- payment metrics + customers.total_payments, + customers.successful_payments, + customers.lifetime_value, + customers.first_payment_at, + customers.last_payment_at, + + -- order metrics + coalesce(order_summary.total_orders, 0) as total_orders, + coalesce(order_summary.total_order_value, 0) as total_order_value, + order_summary.first_order_at, + order_summary.last_order_at, + + -- calculated fields + case + when customers.lifetime_value >= 1000 then 'high' + when customers.lifetime_value >= 100 then 'medium' + else 'low' + end as customer_tier, + + -- timestamps + current_timestamp as _loaded_at + + from customers + left join order_summary using (customer_id) +) + +select * from final +``` + +```sql +-- models/marts/core/fct_orders.sql +{{ + config( + materialized='incremental', + unique_key='order_id', + incremental_strategy='merge' + ) +}} + +with orders as ( + select * from {{ ref('stg_shopify__orders') }} + + {% if is_incremental() %} + where updated_at > (select max(updated_at) from {{ this }}) + {% endif %} +), + +customers as ( + select * from {{ ref('dim_customers') }} +), + +final as ( + select + -- keys + orders.order_id, + customers.customer_key, + orders.customer_id, + + -- dimensions + orders.order_status, + orders.fulfillment_status, + orders.payment_status, + + -- measures + orders.subtotal, + orders.tax, + orders.shipping, + orders.total_price, + orders.total_discount, + orders.item_count, + + -- timestamps + orders.created_at, + orders.updated_at, + orders.fulfilled_at, + + -- metadata + current_timestamp as _loaded_at + + from orders + left join customers on orders.customer_id = customers.customer_id +) + +select * from final +``` + +### Pattern 5: Testing and Documentation + +```yaml +# models/marts/core/_core__models.yml +version: 2 + +models: + - name: dim_customers + description: Customer dimension with payment and order metrics + columns: + - name: customer_key + description: Surrogate key for the customer dimension + tests: + - unique + - not_null + + - name: customer_id + description: Natural key from source system + tests: + - unique + - not_null + + - name: email + description: Customer email address + tests: + - not_null + + - name: customer_tier + description: Customer value tier based on lifetime value + tests: + - accepted_values: + values: ['high', 'medium', 'low'] + + - name: lifetime_value + description: Total amount paid by customer + tests: + - dbt_utils.expression_is_true: + expression: ">= 0" + + - name: fct_orders + description: Order fact table with all order transactions + tests: + - dbt_utils.recency: + datepart: day + field: created_at + interval: 1 + columns: + - name: order_id + tests: + - unique + - not_null + - name: customer_key + tests: + - not_null + - relationships: + to: ref('dim_customers') + field: customer_key +``` + +### Pattern 6: Macros and DRY Code + +```sql +-- macros/cents_to_dollars.sql +{% macro cents_to_dollars(column_name, precision=2) %} + round({{ column_name }} / 100.0, {{ precision }}) +{% endmacro %} + +-- macros/generate_schema_name.sql +{% macro generate_schema_name(custom_schema_name, node) %} + {%- set default_schema = target.schema -%} + {%- if custom_schema_name is none -%} + {{ default_schema }} + {%- else -%} + {{ default_schema }}_{{ custom_schema_name }} + {%- endif -%} +{% endmacro %} + +-- macros/limit_data_in_dev.sql +{% macro limit_data_in_dev(column_name, days=3) %} + {% if target.name == 'dev' %} + where {{ column_name }} >= dateadd(day, -{{ days }}, current_date) + {% endif %} +{% endmacro %} + +-- Usage in model +select * from {{ ref('stg_orders') }} +{{ limit_data_in_dev('created_at') }} +``` + +### Pattern 7: Incremental Strategies + +```sql +-- Delete+Insert (default for most warehouses) +{{ + config( + materialized='incremental', + unique_key='id', + incremental_strategy='delete+insert' + ) +}} + +-- Merge (best for late-arriving data) +{{ + config( + materialized='incremental', + unique_key='id', + incremental_strategy='merge', + merge_update_columns=['status', 'amount', 'updated_at'] + ) +}} + +-- Insert Overwrite (partition-based) +{{ + config( + materialized='incremental', + incremental_strategy='insert_overwrite', + partition_by={ + "field": "created_date", + "data_type": "date", + "granularity": "day" + } + ) +}} + +select + *, + date(created_at) as created_date +from {{ ref('stg_events') }} + +{% if is_incremental() %} +where created_date >= dateadd(day, -3, current_date) +{% endif %} +``` + +## dbt Commands + +```bash +# Development +dbt run # Run all models +dbt run --select staging # Run staging models only +dbt run --select +fct_orders # Run fct_orders and its upstream +dbt run --select fct_orders+ # Run fct_orders and its downstream +dbt run --full-refresh # Rebuild incremental models + +# Testing +dbt test # Run all tests +dbt test --select stg_stripe # Test specific models +dbt build # Run + test in DAG order + +# Documentation +dbt docs generate # Generate docs +dbt docs serve # Serve docs locally + +# Debugging +dbt compile # Compile SQL without running +dbt debug # Test connection +dbt ls --select tag:critical # List models by tag +``` + +## Best Practices + +### Do's +- **Use staging layer** - Clean data once, use everywhere +- **Test aggressively** - Not null, unique, relationships +- **Document everything** - Column descriptions, model descriptions +- **Use incremental** - For tables > 1M rows +- **Version control** - dbt project in Git + +### Don'ts +- **Don't skip staging** - Raw → mart is tech debt +- **Don't hardcode dates** - Use `{{ var('start_date') }}` +- **Don't repeat logic** - Extract to macros +- **Don't test in prod** - Use dev target +- **Don't ignore freshness** - Monitor source data + +## Resources + +- [dbt Documentation](https://docs.getdbt.com/) +- [dbt Best Practices](https://docs.getdbt.com/guides/best-practices) +- [dbt-utils Package](https://hub.getdbt.com/dbt-labs/dbt_utils/latest/) +- [dbt Discourse](https://discourse.getdbt.com/) diff --git a/plugins/data-engineering/skills/spark-optimization/SKILL.md b/plugins/data-engineering/skills/spark-optimization/SKILL.md new file mode 100644 index 0000000..1e21838 --- /dev/null +++ b/plugins/data-engineering/skills/spark-optimization/SKILL.md @@ -0,0 +1,415 @@ +--- +name: spark-optimization +description: Optimize Apache Spark jobs with partitioning, caching, shuffle optimization, and memory tuning. Use when improving Spark performance, debugging slow jobs, or scaling data processing pipelines. +--- + +# Apache Spark Optimization + +Production patterns for optimizing Apache Spark jobs including partitioning strategies, memory management, shuffle optimization, and performance tuning. + +## When to Use This Skill + +- Optimizing slow Spark jobs +- Tuning memory and executor configuration +- Implementing efficient partitioning strategies +- Debugging Spark performance issues +- Scaling Spark pipelines for large datasets +- Reducing shuffle and data skew + +## Core Concepts + +### 1. Spark Execution Model + +``` +Driver Program + ↓ +Job (triggered by action) + ↓ +Stages (separated by shuffles) + ↓ +Tasks (one per partition) +``` + +### 2. Key Performance Factors + +| Factor | Impact | Solution | +|--------|--------|----------| +| **Shuffle** | Network I/O, disk I/O | Minimize wide transformations | +| **Data Skew** | Uneven task duration | Salting, broadcast joins | +| **Serialization** | CPU overhead | Use Kryo, columnar formats | +| **Memory** | GC pressure, spills | Tune executor memory | +| **Partitions** | Parallelism | Right-size partitions | + +## Quick Start + +```python +from pyspark.sql import SparkSession +from pyspark.sql import functions as F + +# Create optimized Spark session +spark = (SparkSession.builder + .appName("OptimizedJob") + .config("spark.sql.adaptive.enabled", "true") + .config("spark.sql.adaptive.coalescePartitions.enabled", "true") + .config("spark.sql.adaptive.skewJoin.enabled", "true") + .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer") + .config("spark.sql.shuffle.partitions", "200") + .getOrCreate()) + +# Read with optimized settings +df = (spark.read + .format("parquet") + .option("mergeSchema", "false") + .load("s3://bucket/data/")) + +# Efficient transformations +result = (df + .filter(F.col("date") >= "2024-01-01") + .select("id", "amount", "category") + .groupBy("category") + .agg(F.sum("amount").alias("total"))) + +result.write.mode("overwrite").parquet("s3://bucket/output/") +``` + +## Patterns + +### Pattern 1: Optimal Partitioning + +```python +# Calculate optimal partition count +def calculate_partitions(data_size_gb: float, partition_size_mb: int = 128) -> int: + """ + Optimal partition size: 128MB - 256MB + Too few: Under-utilization, memory pressure + Too many: Task scheduling overhead + """ + return max(int(data_size_gb * 1024 / partition_size_mb), 1) + +# Repartition for even distribution +df_repartitioned = df.repartition(200, "partition_key") + +# Coalesce to reduce partitions (no shuffle) +df_coalesced = df.coalesce(100) + +# Partition pruning with predicate pushdown +df = (spark.read.parquet("s3://bucket/data/") + .filter(F.col("date") == "2024-01-01")) # Spark pushes this down + +# Write with partitioning for future queries +(df.write + .partitionBy("year", "month", "day") + .mode("overwrite") + .parquet("s3://bucket/partitioned_output/")) +``` + +### Pattern 2: Join Optimization + +```python +from pyspark.sql import functions as F +from pyspark.sql.types import * + +# 1. Broadcast Join - Small table joins +# Best when: One side < 10MB (configurable) +small_df = spark.read.parquet("s3://bucket/small_table/") # < 10MB +large_df = spark.read.parquet("s3://bucket/large_table/") # TBs + +# Explicit broadcast hint +result = large_df.join( + F.broadcast(small_df), + on="key", + how="left" +) + +# 2. Sort-Merge Join - Default for large tables +# Requires shuffle, but handles any size +result = large_df1.join(large_df2, on="key", how="inner") + +# 3. Bucket Join - Pre-sorted, no shuffle at join time +# Write bucketed tables +(df.write + .bucketBy(200, "customer_id") + .sortBy("customer_id") + .mode("overwrite") + .saveAsTable("bucketed_orders")) + +# Join bucketed tables (no shuffle!) +orders = spark.table("bucketed_orders") +customers = spark.table("bucketed_customers") # Same bucket count +result = orders.join(customers, on="customer_id") + +# 4. Skew Join Handling +# Enable AQE skew join optimization +spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true") +spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5") +spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256MB") + +# Manual salting for severe skew +def salt_join(df_skewed, df_other, key_col, num_salts=10): + """Add salt to distribute skewed keys""" + # Add salt to skewed side + df_salted = df_skewed.withColumn( + "salt", + (F.rand() * num_salts).cast("int") + ).withColumn( + "salted_key", + F.concat(F.col(key_col), F.lit("_"), F.col("salt")) + ) + + # Explode other side with all salts + df_exploded = df_other.crossJoin( + spark.range(num_salts).withColumnRenamed("id", "salt") + ).withColumn( + "salted_key", + F.concat(F.col(key_col), F.lit("_"), F.col("salt")) + ) + + # Join on salted key + return df_salted.join(df_exploded, on="salted_key", how="inner") +``` + +### Pattern 3: Caching and Persistence + +```python +from pyspark import StorageLevel + +# Cache when reusing DataFrame multiple times +df = spark.read.parquet("s3://bucket/data/") +df_filtered = df.filter(F.col("status") == "active") + +# Cache in memory (MEMORY_AND_DISK is default) +df_filtered.cache() + +# Or with specific storage level +df_filtered.persist(StorageLevel.MEMORY_AND_DISK_SER) + +# Force materialization +df_filtered.count() + +# Use in multiple actions +agg1 = df_filtered.groupBy("category").count() +agg2 = df_filtered.groupBy("region").sum("amount") + +# Unpersist when done +df_filtered.unpersist() + +# Storage levels explained: +# MEMORY_ONLY - Fast, but may not fit +# MEMORY_AND_DISK - Spills to disk if needed (recommended) +# MEMORY_ONLY_SER - Serialized, less memory, more CPU +# DISK_ONLY - When memory is tight +# OFF_HEAP - Tungsten off-heap memory + +# Checkpoint for complex lineage +spark.sparkContext.setCheckpointDir("s3://bucket/checkpoints/") +df_complex = (df + .join(other_df, "key") + .groupBy("category") + .agg(F.sum("amount"))) +df_complex.checkpoint() # Breaks lineage, materializes +``` + +### Pattern 4: Memory Tuning + +```python +# Executor memory configuration +# spark-submit --executor-memory 8g --executor-cores 4 + +# Memory breakdown (8GB executor): +# - spark.memory.fraction = 0.6 (60% = 4.8GB for execution + storage) +# - spark.memory.storageFraction = 0.5 (50% of 4.8GB = 2.4GB for cache) +# - Remaining 2.4GB for execution (shuffles, joins, sorts) +# - 40% = 3.2GB for user data structures and internal metadata + +spark = (SparkSession.builder + .config("spark.executor.memory", "8g") + .config("spark.executor.memoryOverhead", "2g") # For non-JVM memory + .config("spark.memory.fraction", "0.6") + .config("spark.memory.storageFraction", "0.5") + .config("spark.sql.shuffle.partitions", "200") + # For memory-intensive operations + .config("spark.sql.autoBroadcastJoinThreshold", "50MB") + # Prevent OOM on large shuffles + .config("spark.sql.files.maxPartitionBytes", "128MB") + .getOrCreate()) + +# Monitor memory usage +def print_memory_usage(spark): + """Print current memory usage""" + sc = spark.sparkContext + for executor in sc._jsc.sc().getExecutorMemoryStatus().keySet().toArray(): + mem_status = sc._jsc.sc().getExecutorMemoryStatus().get(executor) + total = mem_status._1() / (1024**3) + free = mem_status._2() / (1024**3) + print(f"{executor}: {total:.2f}GB total, {free:.2f}GB free") +``` + +### Pattern 5: Shuffle Optimization + +```python +# Reduce shuffle data size +spark.conf.set("spark.sql.shuffle.partitions", "auto") # With AQE +spark.conf.set("spark.shuffle.compress", "true") +spark.conf.set("spark.shuffle.spill.compress", "true") + +# Pre-aggregate before shuffle +df_optimized = (df + # Local aggregation first (combiner) + .groupBy("key", "partition_col") + .agg(F.sum("value").alias("partial_sum")) + # Then global aggregation + .groupBy("key") + .agg(F.sum("partial_sum").alias("total"))) + +# Avoid shuffle with map-side operations +# BAD: Shuffle for each distinct +distinct_count = df.select("category").distinct().count() + +# GOOD: Approximate distinct (no shuffle) +approx_count = df.select(F.approx_count_distinct("category")).collect()[0][0] + +# Use coalesce instead of repartition when reducing partitions +df_reduced = df.coalesce(10) # No shuffle + +# Optimize shuffle with compression +spark.conf.set("spark.io.compression.codec", "lz4") # Fast compression +``` + +### Pattern 6: Data Format Optimization + +```python +# Parquet optimizations +(df.write + .option("compression", "snappy") # Fast compression + .option("parquet.block.size", 128 * 1024 * 1024) # 128MB row groups + .parquet("s3://bucket/output/")) + +# Column pruning - only read needed columns +df = (spark.read.parquet("s3://bucket/data/") + .select("id", "amount", "date")) # Spark only reads these columns + +# Predicate pushdown - filter at storage level +df = (spark.read.parquet("s3://bucket/partitioned/year=2024/") + .filter(F.col("status") == "active")) # Pushed to Parquet reader + +# Delta Lake optimizations +(df.write + .format("delta") + .option("optimizeWrite", "true") # Bin-packing + .option("autoCompact", "true") # Compact small files + .mode("overwrite") + .save("s3://bucket/delta_table/")) + +# Z-ordering for multi-dimensional queries +spark.sql(""" + OPTIMIZE delta.`s3://bucket/delta_table/` + ZORDER BY (customer_id, date) +""") +``` + +### Pattern 7: Monitoring and Debugging + +```python +# Enable detailed metrics +spark.conf.set("spark.sql.codegen.wholeStage", "true") +spark.conf.set("spark.sql.execution.arrow.pyspark.enabled", "true") + +# Explain query plan +df.explain(mode="extended") +# Modes: simple, extended, codegen, cost, formatted + +# Get physical plan statistics +df.explain(mode="cost") + +# Monitor task metrics +def analyze_stage_metrics(spark): + """Analyze recent stage metrics""" + status_tracker = spark.sparkContext.statusTracker() + + for stage_id in status_tracker.getActiveStageIds(): + stage_info = status_tracker.getStageInfo(stage_id) + print(f"Stage {stage_id}:") + print(f" Tasks: {stage_info.numTasks}") + print(f" Completed: {stage_info.numCompletedTasks}") + print(f" Failed: {stage_info.numFailedTasks}") + +# Identify data skew +def check_partition_skew(df): + """Check for partition skew""" + partition_counts = (df + .withColumn("partition_id", F.spark_partition_id()) + .groupBy("partition_id") + .count() + .orderBy(F.desc("count"))) + + partition_counts.show(20) + + stats = partition_counts.select( + F.min("count").alias("min"), + F.max("count").alias("max"), + F.avg("count").alias("avg"), + F.stddev("count").alias("stddev") + ).collect()[0] + + skew_ratio = stats["max"] / stats["avg"] + print(f"Skew ratio: {skew_ratio:.2f}x (>2x indicates skew)") +``` + +## Configuration Cheat Sheet + +```python +# Production configuration template +spark_configs = { + # Adaptive Query Execution (AQE) + "spark.sql.adaptive.enabled": "true", + "spark.sql.adaptive.coalescePartitions.enabled": "true", + "spark.sql.adaptive.skewJoin.enabled": "true", + + # Memory + "spark.executor.memory": "8g", + "spark.executor.memoryOverhead": "2g", + "spark.memory.fraction": "0.6", + "spark.memory.storageFraction": "0.5", + + # Parallelism + "spark.sql.shuffle.partitions": "200", + "spark.default.parallelism": "200", + + # Serialization + "spark.serializer": "org.apache.spark.serializer.KryoSerializer", + "spark.sql.execution.arrow.pyspark.enabled": "true", + + # Compression + "spark.io.compression.codec": "lz4", + "spark.shuffle.compress": "true", + + # Broadcast + "spark.sql.autoBroadcastJoinThreshold": "50MB", + + # File handling + "spark.sql.files.maxPartitionBytes": "128MB", + "spark.sql.files.openCostInBytes": "4MB", +} +``` + +## Best Practices + +### Do's +- **Enable AQE** - Adaptive query execution handles many issues +- **Use Parquet/Delta** - Columnar formats with compression +- **Broadcast small tables** - Avoid shuffle for small joins +- **Monitor Spark UI** - Check for skew, spills, GC +- **Right-size partitions** - 128MB - 256MB per partition + +### Don'ts +- **Don't collect large data** - Keep data distributed +- **Don't use UDFs unnecessarily** - Use built-in functions +- **Don't over-cache** - Memory is limited +- **Don't ignore data skew** - It dominates job time +- **Don't use `.count()` for existence** - Use `.take(1)` or `.isEmpty()` + +## Resources + +- [Spark Performance Tuning](https://spark.apache.org/docs/latest/sql-performance-tuning.html) +- [Spark Configuration](https://spark.apache.org/docs/latest/configuration.html) +- [Databricks Optimization Guide](https://docs.databricks.com/en/optimizations/index.html) diff --git a/plugins/developer-essentials/agents/monorepo-architect.md b/plugins/developer-essentials/agents/monorepo-architect.md new file mode 100644 index 0000000..bb40888 --- /dev/null +++ b/plugins/developer-essentials/agents/monorepo-architect.md @@ -0,0 +1,44 @@ +# Monorepo Architect + +Expert in monorepo architecture, build systems, and dependency management at scale. Masters Nx, Turborepo, Bazel, and Lerna for efficient multi-project development. Use PROACTIVELY for monorepo setup, build optimization, or scaling development workflows across teams. + +## Capabilities + +- Monorepo tool selection (Nx, Turborepo, Bazel, Lerna) +- Workspace configuration and project structure +- Build caching (local and remote) +- Dependency graph management +- Affected/changed detection for CI optimization +- Code sharing and library extraction +- Task orchestration and parallelization + +## When to Use + +- Setting up a new monorepo from scratch +- Migrating from polyrepo to monorepo +- Optimizing slow CI/CD pipelines +- Sharing code between multiple applications +- Managing dependencies across projects +- Implementing consistent tooling across teams + +## Workflow + +1. Assess codebase size and team structure +2. Select appropriate monorepo tooling +3. Design workspace and project structure +4. Configure build caching strategy +5. Set up affected/changed detection +6. Implement task pipelines +7. Configure remote caching for CI +8. Document conventions and workflows + +## Best Practices + +- Start with clear project boundaries +- Use consistent naming conventions +- Implement remote caching early +- Keep shared libraries focused +- Use tags for dependency constraints +- Automate dependency updates +- Document the dependency graph +- Set up code ownership rules diff --git a/plugins/developer-essentials/skills/bazel-build-optimization/SKILL.md b/plugins/developer-essentials/skills/bazel-build-optimization/SKILL.md new file mode 100644 index 0000000..934a8b7 --- /dev/null +++ b/plugins/developer-essentials/skills/bazel-build-optimization/SKILL.md @@ -0,0 +1,385 @@ +--- +name: bazel-build-optimization +description: Optimize Bazel builds for large-scale monorepos. Use when configuring Bazel, implementing remote execution, or optimizing build performance for enterprise codebases. +--- + +# Bazel Build Optimization + +Production patterns for Bazel in large-scale monorepos. + +## When to Use This Skill + +- Setting up Bazel for monorepos +- Configuring remote caching/execution +- Optimizing build times +- Writing custom Bazel rules +- Debugging build issues +- Migrating to Bazel + +## Core Concepts + +### 1. Bazel Architecture + +``` +workspace/ +├── WORKSPACE.bazel # External dependencies +├── .bazelrc # Build configurations +├── .bazelversion # Bazel version +├── BUILD.bazel # Root build file +├── apps/ +│ └── web/ +│ └── BUILD.bazel +├── libs/ +│ └── utils/ +│ └── BUILD.bazel +└── tools/ + └── bazel/ + └── rules/ +``` + +### 2. Key Concepts + +| Concept | Description | +|---------|-------------| +| **Target** | Buildable unit (library, binary, test) | +| **Package** | Directory with BUILD file | +| **Label** | Target identifier `//path/to:target` | +| **Rule** | Defines how to build a target | +| **Aspect** | Cross-cutting build behavior | + +## Templates + +### Template 1: WORKSPACE Configuration + +```python +# WORKSPACE.bazel +workspace(name = "myproject") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Rules for JavaScript/TypeScript +http_archive( + name = "aspect_rules_js", + sha256 = "...", + strip_prefix = "rules_js-1.34.0", + url = "https://github.com/aspect-build/rules_js/releases/download/v1.34.0/rules_js-v1.34.0.tar.gz", +) + +load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies") +rules_js_dependencies() + +load("@rules_nodejs//nodejs:repositories.bzl", "nodejs_register_toolchains") +nodejs_register_toolchains( + name = "nodejs", + node_version = "20.9.0", +) + +load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock") +npm_translate_lock( + name = "npm", + pnpm_lock = "//:pnpm-lock.yaml", + verify_node_modules_ignored = "//:.bazelignore", +) + +load("@npm//:repositories.bzl", "npm_repositories") +npm_repositories() + +# Rules for Python +http_archive( + name = "rules_python", + sha256 = "...", + strip_prefix = "rules_python-0.27.0", + url = "https://github.com/bazelbuild/rules_python/releases/download/0.27.0/rules_python-0.27.0.tar.gz", +) + +load("@rules_python//python:repositories.bzl", "py_repositories") +py_repositories() +``` + +### Template 2: .bazelrc Configuration + +```bash +# .bazelrc + +# Build settings +build --enable_platform_specific_config +build --incompatible_enable_cc_toolchain_resolution +build --experimental_strict_conflict_checks + +# Performance +build --jobs=auto +build --local_cpu_resources=HOST_CPUS*.75 +build --local_ram_resources=HOST_RAM*.75 + +# Caching +build --disk_cache=~/.cache/bazel-disk +build --repository_cache=~/.cache/bazel-repo + +# Remote caching (optional) +build:remote-cache --remote_cache=grpcs://cache.example.com +build:remote-cache --remote_upload_local_results=true +build:remote-cache --remote_timeout=3600 + +# Remote execution (optional) +build:remote-exec --remote_executor=grpcs://remote.example.com +build:remote-exec --remote_instance_name=projects/myproject/instances/default +build:remote-exec --jobs=500 + +# Platform configurations +build:linux --platforms=//platforms:linux_x86_64 +build:macos --platforms=//platforms:macos_arm64 + +# CI configuration +build:ci --config=remote-cache +build:ci --build_metadata=ROLE=CI +build:ci --bes_results_url=https://results.example.com/invocation/ +build:ci --bes_backend=grpcs://bes.example.com + +# Test settings +test --test_output=errors +test --test_summary=detailed + +# Coverage +coverage --combined_report=lcov +coverage --instrumentation_filter="//..." + +# Convenience aliases +build:opt --compilation_mode=opt +build:dbg --compilation_mode=dbg + +# Import user settings +try-import %workspace%/user.bazelrc +``` + +### Template 3: TypeScript Library BUILD + +```python +# libs/utils/BUILD.bazel +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") +load("@aspect_rules_js//js:defs.bzl", "js_library") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages(name = "node_modules") + +ts_project( + name = "utils_ts", + srcs = glob(["src/**/*.ts"]), + declaration = True, + source_map = True, + tsconfig = "//:tsconfig.json", + deps = [ + ":node_modules/@types/node", + ], +) + +js_library( + name = "utils", + srcs = [":utils_ts"], + visibility = ["//visibility:public"], +) + +# Tests +load("@aspect_rules_jest//jest:defs.bzl", "jest_test") + +jest_test( + name = "utils_test", + config = "//:jest.config.js", + data = [ + ":utils", + "//:node_modules/jest", + ], + node_modules = "//:node_modules", +) +``` + +### Template 4: Python Library BUILD + +```python +# libs/ml/BUILD.bazel +load("@rules_python//python:defs.bzl", "py_library", "py_test", "py_binary") +load("@pip//:requirements.bzl", "requirement") + +py_library( + name = "ml", + srcs = glob(["src/**/*.py"]), + deps = [ + requirement("numpy"), + requirement("pandas"), + requirement("scikit-learn"), + "//libs/utils:utils_py", + ], + visibility = ["//visibility:public"], +) + +py_test( + name = "ml_test", + srcs = glob(["tests/**/*.py"]), + deps = [ + ":ml", + requirement("pytest"), + ], + size = "medium", + timeout = "moderate", +) + +py_binary( + name = "train", + srcs = ["train.py"], + deps = [":ml"], + data = ["//data:training_data"], +) +``` + +### Template 5: Custom Rule for Docker + +```python +# tools/bazel/rules/docker.bzl +def _docker_image_impl(ctx): + dockerfile = ctx.file.dockerfile + base_image = ctx.attr.base_image + layers = ctx.files.layers + + # Build the image + output = ctx.actions.declare_file(ctx.attr.name + ".tar") + + args = ctx.actions.args() + args.add("--dockerfile", dockerfile) + args.add("--output", output) + args.add("--base", base_image) + args.add_all("--layer", layers) + + ctx.actions.run( + inputs = [dockerfile] + layers, + outputs = [output], + executable = ctx.executable._builder, + arguments = [args], + mnemonic = "DockerBuild", + progress_message = "Building Docker image %s" % ctx.label, + ) + + return [DefaultInfo(files = depset([output]))] + +docker_image = rule( + implementation = _docker_image_impl, + attrs = { + "dockerfile": attr.label( + allow_single_file = [".dockerfile", "Dockerfile"], + mandatory = True, + ), + "base_image": attr.string(mandatory = True), + "layers": attr.label_list(allow_files = True), + "_builder": attr.label( + default = "//tools/docker:builder", + executable = True, + cfg = "exec", + ), + }, +) +``` + +### Template 6: Query and Dependency Analysis + +```bash +# Find all dependencies of a target +bazel query "deps(//apps/web:web)" + +# Find reverse dependencies (what depends on this) +bazel query "rdeps(//..., //libs/utils:utils)" + +# Find all targets in a package +bazel query "//libs/..." + +# Find changed targets since commit +bazel query "rdeps(//..., set($(git diff --name-only HEAD~1 | sed 's/.*/"&"/' | tr '\n' ' ')))" + +# Generate dependency graph +bazel query "deps(//apps/web:web)" --output=graph | dot -Tpng > deps.png + +# Find all test targets +bazel query "kind('.*_test', //...)" + +# Find targets with specific tag +bazel query "attr(tags, 'integration', //...)" + +# Compute build graph size +bazel query "deps(//...)" --output=package | wc -l +``` + +### Template 7: Remote Execution Setup + +```python +# platforms/BUILD.bazel +platform( + name = "linux_x86_64", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + exec_properties = { + "container-image": "docker://gcr.io/myproject/bazel-worker:latest", + "OSFamily": "Linux", + }, +) + +platform( + name = "remote_linux", + parents = [":linux_x86_64"], + exec_properties = { + "Pool": "default", + "dockerNetwork": "standard", + }, +) + +# toolchains/BUILD.bazel +toolchain( + name = "cc_toolchain_linux", + exec_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + target_compatible_with = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], + toolchain = "@remotejdk11_linux//:jdk", + toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type", +) +``` + +## Performance Optimization + +```bash +# Profile build +bazel build //... --profile=profile.json +bazel analyze-profile profile.json + +# Identify slow actions +bazel build //... --execution_log_json_file=exec_log.json + +# Memory profiling +bazel build //... --memory_profile=memory.json + +# Skip analysis cache +bazel build //... --notrack_incremental_state +``` + +## Best Practices + +### Do's +- **Use fine-grained targets** - Better caching +- **Pin dependencies** - Reproducible builds +- **Enable remote caching** - Share build artifacts +- **Use visibility wisely** - Enforce architecture +- **Write BUILD files per directory** - Standard convention + +### Don'ts +- **Don't use glob for deps** - Explicit is better +- **Don't commit bazel-* dirs** - Add to .gitignore +- **Don't skip WORKSPACE setup** - Foundation of build +- **Don't ignore build warnings** - Technical debt + +## Resources + +- [Bazel Documentation](https://bazel.build/docs) +- [Bazel Remote Execution](https://bazel.build/docs/remote-execution) +- [rules_js](https://github.com/aspect-build/rules_js) diff --git a/plugins/developer-essentials/skills/nx-workspace-patterns/SKILL.md b/plugins/developer-essentials/skills/nx-workspace-patterns/SKILL.md new file mode 100644 index 0000000..0fd4616 --- /dev/null +++ b/plugins/developer-essentials/skills/nx-workspace-patterns/SKILL.md @@ -0,0 +1,452 @@ +--- +name: nx-workspace-patterns +description: Configure and optimize Nx monorepo workspaces. Use when setting up Nx, configuring project boundaries, optimizing build caching, or implementing affected commands. +--- + +# Nx Workspace Patterns + +Production patterns for Nx monorepo management. + +## When to Use This Skill + +- Setting up new Nx workspaces +- Configuring project boundaries +- Optimizing CI with affected commands +- Implementing remote caching +- Managing dependencies between projects +- Migrating to Nx + +## Core Concepts + +### 1. Nx Architecture + +``` +workspace/ +├── apps/ # Deployable applications +│ ├── web/ +│ └── api/ +├── libs/ # Shared libraries +│ ├── shared/ +│ │ ├── ui/ +│ │ └── utils/ +│ └── feature/ +│ ├── auth/ +│ └── dashboard/ +├── tools/ # Custom executors/generators +├── nx.json # Nx configuration +└── workspace.json # Project configuration +``` + +### 2. Library Types + +| Type | Purpose | Example | +|------|---------|---------| +| **feature** | Smart components, business logic | `feature-auth` | +| **ui** | Presentational components | `ui-buttons` | +| **data-access** | API calls, state management | `data-access-users` | +| **util** | Pure functions, helpers | `util-formatting` | +| **shell** | App bootstrapping | `shell-web` | + +## Templates + +### Template 1: nx.json Configuration + +```json +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "npmScope": "myorg", + "affected": { + "defaultBase": "main" + }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": [ + "build", + "lint", + "test", + "e2e", + "build-storybook" + ], + "parallel": 3 + } + } + }, + "targetDefaults": { + "build": { + "dependsOn": ["^build"], + "inputs": ["production", "^production"], + "cache": true + }, + "test": { + "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], + "cache": true + }, + "lint": { + "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], + "cache": true + }, + "e2e": { + "inputs": ["default", "^production"], + "cache": true + } + }, + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "production": [ + "default", + "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", + "!{projectRoot}/tsconfig.spec.json", + "!{projectRoot}/jest.config.[jt]s", + "!{projectRoot}/.eslintrc.json" + ], + "sharedGlobals": [ + "{workspaceRoot}/babel.config.json", + "{workspaceRoot}/tsconfig.base.json" + ] + }, + "generators": { + "@nx/react": { + "application": { + "style": "css", + "linter": "eslint", + "bundler": "webpack" + }, + "library": { + "style": "css", + "linter": "eslint" + }, + "component": { + "style": "css" + } + } + } +} +``` + +### Template 2: Project Configuration + +```json +// apps/web/project.json +{ + "name": "web", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/web/src", + "projectType": "application", + "tags": ["type:app", "scope:web"], + "targets": { + "build": { + "executor": "@nx/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "compiler": "babel", + "outputPath": "dist/apps/web", + "index": "apps/web/src/index.html", + "main": "apps/web/src/main.tsx", + "tsConfig": "apps/web/tsconfig.app.json", + "assets": ["apps/web/src/assets"], + "styles": ["apps/web/src/styles.css"] + }, + "configurations": { + "development": { + "extractLicenses": false, + "optimization": false, + "sourceMap": true + }, + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractLicenses": true + } + } + }, + "serve": { + "executor": "@nx/webpack:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "web:build" + }, + "configurations": { + "development": { + "buildTarget": "web:build:development" + }, + "production": { + "buildTarget": "web:build:production" + } + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/web/jest.config.ts", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/web/**/*.{ts,tsx,js,jsx}"] + } + } + } +} +``` + +### Template 3: Module Boundary Rules + +```json +// .eslintrc.json +{ + "root": true, + "ignorePatterns": ["**/*"], + "plugins": ["@nx"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": { + "@nx/enforce-module-boundaries": [ + "error", + { + "enforceBuildableLibDependency": true, + "allow": [], + "depConstraints": [ + { + "sourceTag": "type:app", + "onlyDependOnLibsWithTags": [ + "type:feature", + "type:ui", + "type:data-access", + "type:util" + ] + }, + { + "sourceTag": "type:feature", + "onlyDependOnLibsWithTags": [ + "type:ui", + "type:data-access", + "type:util" + ] + }, + { + "sourceTag": "type:ui", + "onlyDependOnLibsWithTags": ["type:ui", "type:util"] + }, + { + "sourceTag": "type:data-access", + "onlyDependOnLibsWithTags": ["type:data-access", "type:util"] + }, + { + "sourceTag": "type:util", + "onlyDependOnLibsWithTags": ["type:util"] + }, + { + "sourceTag": "scope:web", + "onlyDependOnLibsWithTags": ["scope:web", "scope:shared"] + }, + { + "sourceTag": "scope:api", + "onlyDependOnLibsWithTags": ["scope:api", "scope:shared"] + }, + { + "sourceTag": "scope:shared", + "onlyDependOnLibsWithTags": ["scope:shared"] + } + ] + } + ] + } + } + ] +} +``` + +### Template 4: Custom Generator + +```typescript +// tools/generators/feature-lib/index.ts +import { + Tree, + formatFiles, + generateFiles, + joinPathFragments, + names, + readProjectConfiguration, +} from '@nx/devkit'; +import { libraryGenerator } from '@nx/react'; + +interface FeatureLibraryGeneratorSchema { + name: string; + scope: string; + directory?: string; +} + +export default async function featureLibraryGenerator( + tree: Tree, + options: FeatureLibraryGeneratorSchema +) { + const { name, scope, directory } = options; + const projectDirectory = directory + ? `${directory}/${name}` + : `libs/${scope}/feature-${name}`; + + // Generate base library + await libraryGenerator(tree, { + name: `feature-${name}`, + directory: projectDirectory, + tags: `type:feature,scope:${scope}`, + style: 'css', + skipTsConfig: false, + skipFormat: true, + unitTestRunner: 'jest', + linter: 'eslint', + }); + + // Add custom files + const projectConfig = readProjectConfiguration(tree, `${scope}-feature-${name}`); + const projectNames = names(name); + + generateFiles( + tree, + joinPathFragments(__dirname, 'files'), + projectConfig.sourceRoot, + { + ...projectNames, + scope, + tmpl: '', + } + ); + + await formatFiles(tree); +} +``` + +### Template 5: CI Configuration with Affected + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Derive SHAs for affected commands + uses: nrwl/nx-set-shas@v4 + + - name: Run affected lint + run: npx nx affected -t lint --parallel=3 + + - name: Run affected test + run: npx nx affected -t test --parallel=3 --configuration=ci + + - name: Run affected build + run: npx nx affected -t build --parallel=3 + + - name: Run affected e2e + run: npx nx affected -t e2e --parallel=1 +``` + +### Template 6: Remote Caching Setup + +```typescript +// nx.json with Nx Cloud +{ + "tasksRunnerOptions": { + "default": { + "runner": "nx-cloud", + "options": { + "cacheableOperations": ["build", "lint", "test", "e2e"], + "accessToken": "your-nx-cloud-token", + "parallel": 3, + "cacheDirectory": ".nx/cache" + } + } + }, + "nxCloudAccessToken": "your-nx-cloud-token" +} + +// Self-hosted cache with S3 +{ + "tasksRunnerOptions": { + "default": { + "runner": "@nx-aws-cache/nx-aws-cache", + "options": { + "cacheableOperations": ["build", "lint", "test"], + "awsRegion": "us-east-1", + "awsBucket": "my-nx-cache-bucket", + "awsProfile": "default" + } + } + } +} +``` + +## Common Commands + +```bash +# Generate new library +nx g @nx/react:lib feature-auth --directory=libs/web --tags=type:feature,scope:web + +# Run affected tests +nx affected -t test --base=main + +# View dependency graph +nx graph + +# Run specific project +nx build web --configuration=production + +# Reset cache +nx reset + +# Run migrations +nx migrate latest +nx migrate --run-migrations +``` + +## Best Practices + +### Do's +- **Use tags consistently** - Enforce with module boundaries +- **Enable caching early** - Significant CI savings +- **Keep libs focused** - Single responsibility +- **Use generators** - Ensure consistency +- **Document boundaries** - Help new developers + +### Don'ts +- **Don't create circular deps** - Graph should be acyclic +- **Don't skip affected** - Test only what changed +- **Don't ignore boundaries** - Tech debt accumulates +- **Don't over-granularize** - Balance lib count + +## Resources + +- [Nx Documentation](https://nx.dev/getting-started/intro) +- [Module Boundaries](https://nx.dev/core-features/enforce-module-boundaries) +- [Nx Cloud](https://nx.app/) diff --git a/plugins/developer-essentials/skills/turborepo-caching/SKILL.md b/plugins/developer-essentials/skills/turborepo-caching/SKILL.md new file mode 100644 index 0000000..865b8a2 --- /dev/null +++ b/plugins/developer-essentials/skills/turborepo-caching/SKILL.md @@ -0,0 +1,407 @@ +--- +name: turborepo-caching +description: Configure Turborepo for efficient monorepo builds with local and remote caching. Use when setting up Turborepo, optimizing build pipelines, or implementing distributed caching. +--- + +# Turborepo Caching + +Production patterns for Turborepo build optimization. + +## When to Use This Skill + +- Setting up new Turborepo projects +- Configuring build pipelines +- Implementing remote caching +- Optimizing CI/CD performance +- Migrating from other monorepo tools +- Debugging cache misses + +## Core Concepts + +### 1. Turborepo Architecture + +``` +Workspace Root/ +├── apps/ +│ ├── web/ +│ │ └── package.json +│ └── docs/ +│ └── package.json +├── packages/ +│ ├── ui/ +│ │ └── package.json +│ └── config/ +│ └── package.json +├── turbo.json +└── package.json +``` + +### 2. Pipeline Concepts + +| Concept | Description | +|---------|-------------| +| **dependsOn** | Tasks that must complete first | +| **cache** | Whether to cache outputs | +| **outputs** | Files to cache | +| **inputs** | Files that affect cache key | +| **persistent** | Long-running tasks (dev servers) | + +## Templates + +### Template 1: turbo.json Configuration + +```json +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": [ + ".env", + ".env.local" + ], + "globalEnv": [ + "NODE_ENV", + "VERCEL_URL" + ], + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": [ + "dist/**", + ".next/**", + "!.next/cache/**" + ], + "env": [ + "API_URL", + "NEXT_PUBLIC_*" + ] + }, + "test": { + "dependsOn": ["build"], + "outputs": ["coverage/**"], + "inputs": [ + "src/**/*.tsx", + "src/**/*.ts", + "test/**/*.ts" + ] + }, + "lint": { + "outputs": [], + "cache": true + }, + "typecheck": { + "dependsOn": ["^build"], + "outputs": [] + }, + "dev": { + "cache": false, + "persistent": true + }, + "clean": { + "cache": false + } + } +} +``` + +### Template 2: Package-Specific Pipeline + +```json +// apps/web/turbo.json +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "outputs": [".next/**", "!.next/cache/**"], + "env": [ + "NEXT_PUBLIC_API_URL", + "NEXT_PUBLIC_ANALYTICS_ID" + ] + }, + "test": { + "outputs": ["coverage/**"], + "inputs": [ + "src/**", + "tests/**", + "jest.config.js" + ] + } + } +} +``` + +### Template 3: Remote Caching with Vercel + +```bash +# Login to Vercel +npx turbo login + +# Link to Vercel project +npx turbo link + +# Run with remote cache +turbo build --remote-only + +# CI environment variables +TURBO_TOKEN=your-token +TURBO_TEAM=your-team +``` + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npx turbo build --filter='...[origin/main]' + + - name: Test + run: npx turbo test --filter='...[origin/main]' +``` + +### Template 4: Self-Hosted Remote Cache + +```typescript +// Custom remote cache server (Express) +import express from 'express'; +import { createReadStream, createWriteStream } from 'fs'; +import { mkdir } from 'fs/promises'; +import { join } from 'path'; + +const app = express(); +const CACHE_DIR = './cache'; + +// Get artifact +app.get('/v8/artifacts/:hash', async (req, res) => { + const { hash } = req.params; + const team = req.query.teamId || 'default'; + const filePath = join(CACHE_DIR, team, hash); + + try { + const stream = createReadStream(filePath); + stream.pipe(res); + } catch { + res.status(404).send('Not found'); + } +}); + +// Put artifact +app.put('/v8/artifacts/:hash', async (req, res) => { + const { hash } = req.params; + const team = req.query.teamId || 'default'; + const dir = join(CACHE_DIR, team); + const filePath = join(dir, hash); + + await mkdir(dir, { recursive: true }); + + const stream = createWriteStream(filePath); + req.pipe(stream); + + stream.on('finish', () => { + res.json({ urls: [`${req.protocol}://${req.get('host')}/v8/artifacts/${hash}`] }); + }); +}); + +// Check artifact exists +app.head('/v8/artifacts/:hash', async (req, res) => { + const { hash } = req.params; + const team = req.query.teamId || 'default'; + const filePath = join(CACHE_DIR, team, hash); + + try { + await fs.access(filePath); + res.status(200).end(); + } catch { + res.status(404).end(); + } +}); + +app.listen(3000); +``` + +```json +// turbo.json for self-hosted cache +{ + "remoteCache": { + "signature": false + } +} +``` + +```bash +# Use self-hosted cache +turbo build --api="http://localhost:3000" --token="my-token" --team="my-team" +``` + +### Template 5: Filtering and Scoping + +```bash +# Build specific package +turbo build --filter=@myorg/web + +# Build package and its dependencies +turbo build --filter=@myorg/web... + +# Build package and its dependents +turbo build --filter=...@myorg/ui + +# Build changed packages since main +turbo build --filter='...[origin/main]' + +# Build packages in directory +turbo build --filter='./apps/*' + +# Combine filters +turbo build --filter=@myorg/web --filter=@myorg/docs + +# Exclude package +turbo build --filter='!@myorg/docs' + +# Include dependencies of changed +turbo build --filter='...[HEAD^1]...' +``` + +### Template 6: Advanced Pipeline Configuration + +```json +{ + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"], + "inputs": [ + "$TURBO_DEFAULT$", + "!**/*.md", + "!**/*.test.*" + ] + }, + "test": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"], + "inputs": [ + "src/**", + "tests/**", + "*.config.*" + ], + "env": ["CI", "NODE_ENV"] + }, + "test:e2e": { + "dependsOn": ["build"], + "outputs": [], + "cache": false + }, + "deploy": { + "dependsOn": ["build", "test", "lint"], + "outputs": [], + "cache": false + }, + "db:generate": { + "cache": false + }, + "db:push": { + "cache": false, + "dependsOn": ["db:generate"] + }, + "@myorg/web#build": { + "dependsOn": ["^build", "@myorg/db#db:generate"], + "outputs": [".next/**"], + "env": ["NEXT_PUBLIC_*"] + } + } +} +``` + +### Template 7: Root package.json Setup + +```json +{ + "name": "my-turborepo", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "turbo build", + "dev": "turbo dev", + "lint": "turbo lint", + "test": "turbo test", + "clean": "turbo clean && rm -rf node_modules", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "turbo build --filter=./packages/* && changeset publish" + }, + "devDependencies": { + "turbo": "^1.10.0", + "prettier": "^3.0.0", + "@changesets/cli": "^2.26.0" + }, + "packageManager": "npm@10.0.0" +} +``` + +## Debugging Cache + +```bash +# Dry run to see what would run +turbo build --dry-run + +# Verbose output with hashes +turbo build --verbosity=2 + +# Show task graph +turbo build --graph + +# Force no cache +turbo build --force + +# Show cache status +turbo build --summarize + +# Debug specific task +TURBO_LOG_VERBOSITY=debug turbo build --filter=@myorg/web +``` + +## Best Practices + +### Do's +- **Define explicit inputs** - Avoid cache invalidation +- **Use workspace protocol** - `"@myorg/ui": "workspace:*"` +- **Enable remote caching** - Share across CI and local +- **Filter in CI** - Build only affected packages +- **Cache build outputs** - Not source files + +### Don'ts +- **Don't cache dev servers** - Use `persistent: true` +- **Don't include secrets in env** - Use runtime env vars +- **Don't ignore dependsOn** - Causes race conditions +- **Don't over-filter** - May miss dependencies + +## Resources + +- [Turborepo Documentation](https://turbo.build/repo/docs) +- [Caching Guide](https://turbo.build/repo/docs/core-concepts/caching) +- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) diff --git a/plugins/documentation-generation/skills/architecture-decision-records/SKILL.md b/plugins/documentation-generation/skills/architecture-decision-records/SKILL.md new file mode 100644 index 0000000..69f8615 --- /dev/null +++ b/plugins/documentation-generation/skills/architecture-decision-records/SKILL.md @@ -0,0 +1,428 @@ +--- +name: architecture-decision-records +description: Write and maintain Architecture Decision Records (ADRs) following best practices for technical decision documentation. Use when documenting significant technical decisions, reviewing past architectural choices, or establishing decision processes. +--- + +# Architecture Decision Records + +Comprehensive patterns for creating, maintaining, and managing Architecture Decision Records (ADRs) that capture the context and rationale behind significant technical decisions. + +## When to Use This Skill + +- Making significant architectural decisions +- Documenting technology choices +- Recording design trade-offs +- Onboarding new team members +- Reviewing historical decisions +- Establishing decision-making processes + +## Core Concepts + +### 1. What is an ADR? + +An Architecture Decision Record captures: +- **Context**: Why we needed to make a decision +- **Decision**: What we decided +- **Consequences**: What happens as a result + +### 2. When to Write an ADR + +| Write ADR | Skip ADR | +|-----------|----------| +| New framework adoption | Minor version upgrades | +| Database technology choice | Bug fixes | +| API design patterns | Implementation details | +| Security architecture | Routine maintenance | +| Integration patterns | Configuration changes | + +### 3. ADR Lifecycle + +``` +Proposed → Accepted → Deprecated → Superseded + ↓ + Rejected +``` + +## Templates + +### Template 1: Standard ADR (MADR Format) + +```markdown +# ADR-0001: Use PostgreSQL as Primary Database + +## Status + +Accepted + +## Context + +We need to select a primary database for our new e-commerce platform. The system +will handle: +- ~10,000 concurrent users +- Complex product catalog with hierarchical categories +- Transaction processing for orders and payments +- Full-text search for products +- Geospatial queries for store locator + +The team has experience with MySQL, PostgreSQL, and MongoDB. We need ACID +compliance for financial transactions. + +## Decision Drivers + +* **Must have ACID compliance** for payment processing +* **Must support complex queries** for reporting +* **Should support full-text search** to reduce infrastructure complexity +* **Should have good JSON support** for flexible product attributes +* **Team familiarity** reduces onboarding time + +## Considered Options + +### Option 1: PostgreSQL +- **Pros**: ACID compliant, excellent JSON support (JSONB), built-in full-text + search, PostGIS for geospatial, team has experience +- **Cons**: Slightly more complex replication setup than MySQL + +### Option 2: MySQL +- **Pros**: Very familiar to team, simple replication, large community +- **Cons**: Weaker JSON support, no built-in full-text search (need + Elasticsearch), no geospatial without extensions + +### Option 3: MongoDB +- **Pros**: Flexible schema, native JSON, horizontal scaling +- **Cons**: No ACID for multi-document transactions (at decision time), + team has limited experience, requires schema design discipline + +## Decision + +We will use **PostgreSQL 15** as our primary database. + +## Rationale + +PostgreSQL provides the best balance of: +1. **ACID compliance** essential for e-commerce transactions +2. **Built-in capabilities** (full-text search, JSONB, PostGIS) reduce + infrastructure complexity +3. **Team familiarity** with SQL databases reduces learning curve +4. **Mature ecosystem** with excellent tooling and community support + +The slight complexity in replication is outweighed by the reduction in +additional services (no separate Elasticsearch needed). + +## Consequences + +### Positive +- Single database handles transactions, search, and geospatial queries +- Reduced operational complexity (fewer services to manage) +- Strong consistency guarantees for financial data +- Team can leverage existing SQL expertise + +### Negative +- Need to learn PostgreSQL-specific features (JSONB, full-text search syntax) +- Vertical scaling limits may require read replicas sooner +- Some team members need PostgreSQL-specific training + +### Risks +- Full-text search may not scale as well as dedicated search engines +- Mitigation: Design for potential Elasticsearch addition if needed + +## Implementation Notes + +- Use JSONB for flexible product attributes +- Implement connection pooling with PgBouncer +- Set up streaming replication for read replicas +- Use pg_trgm extension for fuzzy search + +## Related Decisions + +- ADR-0002: Caching Strategy (Redis) - complements database choice +- ADR-0005: Search Architecture - may supersede if Elasticsearch needed + +## References + +- [PostgreSQL JSON Documentation](https://www.postgresql.org/docs/current/datatype-json.html) +- [PostgreSQL Full Text Search](https://www.postgresql.org/docs/current/textsearch.html) +- Internal: Performance benchmarks in `/docs/benchmarks/database-comparison.md` +``` + +### Template 2: Lightweight ADR + +```markdown +# ADR-0012: Adopt TypeScript for Frontend Development + +**Status**: Accepted +**Date**: 2024-01-15 +**Deciders**: @alice, @bob, @charlie + +## Context + +Our React codebase has grown to 50+ components with increasing bug reports +related to prop type mismatches and undefined errors. PropTypes provide +runtime-only checking. + +## Decision + +Adopt TypeScript for all new frontend code. Migrate existing code incrementally. + +## Consequences + +**Good**: Catch type errors at compile time, better IDE support, self-documenting +code. + +**Bad**: Learning curve for team, initial slowdown, build complexity increase. + +**Mitigations**: TypeScript training sessions, allow gradual adoption with +`allowJs: true`. +``` + +### Template 3: Y-Statement Format + +```markdown +# ADR-0015: API Gateway Selection + +In the context of **building a microservices architecture**, +facing **the need for centralized API management, authentication, and rate limiting**, +we decided for **Kong Gateway** +and against **AWS API Gateway and custom Nginx solution**, +to achieve **vendor independence, plugin extensibility, and team familiarity with Lua**, +accepting that **we need to manage Kong infrastructure ourselves**. +``` + +### Template 4: ADR for Deprecation + +```markdown +# ADR-0020: Deprecate MongoDB in Favor of PostgreSQL + +## Status + +Accepted (Supersedes ADR-0003) + +## Context + +ADR-0003 (2021) chose MongoDB for user profile storage due to schema flexibility +needs. Since then: +- MongoDB's multi-document transactions remain problematic for our use case +- Our schema has stabilized and rarely changes +- We now have PostgreSQL expertise from other services +- Maintaining two databases increases operational burden + +## Decision + +Deprecate MongoDB and migrate user profiles to PostgreSQL. + +## Migration Plan + +1. **Phase 1** (Week 1-2): Create PostgreSQL schema, dual-write enabled +2. **Phase 2** (Week 3-4): Backfill historical data, validate consistency +3. **Phase 3** (Week 5): Switch reads to PostgreSQL, monitor +4. **Phase 4** (Week 6): Remove MongoDB writes, decommission + +## Consequences + +### Positive +- Single database technology reduces operational complexity +- ACID transactions for user data +- Team can focus PostgreSQL expertise + +### Negative +- Migration effort (~4 weeks) +- Risk of data issues during migration +- Lose some schema flexibility + +## Lessons Learned + +Document from ADR-0003 experience: +- Schema flexibility benefits were overestimated +- Operational cost of multiple databases was underestimated +- Consider long-term maintenance in technology decisions +``` + +### Template 5: Request for Comments (RFC) Style + +```markdown +# RFC-0025: Adopt Event Sourcing for Order Management + +## Summary + +Propose adopting event sourcing pattern for the order management domain to +improve auditability, enable temporal queries, and support business analytics. + +## Motivation + +Current challenges: +1. Audit requirements need complete order history +2. "What was the order state at time X?" queries are impossible +3. Analytics team needs event stream for real-time dashboards +4. Order state reconstruction for customer support is manual + +## Detailed Design + +### Event Store + +``` +OrderCreated { orderId, customerId, items[], timestamp } +OrderItemAdded { orderId, item, timestamp } +OrderItemRemoved { orderId, itemId, timestamp } +PaymentReceived { orderId, amount, paymentId, timestamp } +OrderShipped { orderId, trackingNumber, timestamp } +``` + +### Projections + +- **CurrentOrderState**: Materialized view for queries +- **OrderHistory**: Complete timeline for audit +- **DailyOrderMetrics**: Analytics aggregation + +### Technology + +- Event Store: EventStoreDB (purpose-built, handles projections) +- Alternative considered: Kafka + custom projection service + +## Drawbacks + +- Learning curve for team +- Increased complexity vs. CRUD +- Need to design events carefully (immutable once stored) +- Storage growth (events never deleted) + +## Alternatives + +1. **Audit tables**: Simpler but doesn't enable temporal queries +2. **CDC from existing DB**: Complex, doesn't change data model +3. **Hybrid**: Event source only for order state changes + +## Unresolved Questions + +- [ ] Event schema versioning strategy +- [ ] Retention policy for events +- [ ] Snapshot frequency for performance + +## Implementation Plan + +1. Prototype with single order type (2 weeks) +2. Team training on event sourcing (1 week) +3. Full implementation and migration (4 weeks) +4. Monitoring and optimization (ongoing) + +## References + +- [Event Sourcing by Martin Fowler](https://martinfowler.com/eaaDev/EventSourcing.html) +- [EventStoreDB Documentation](https://www.eventstore.com/docs) +``` + +## ADR Management + +### Directory Structure + +``` +docs/ +├── adr/ +│ ├── README.md # Index and guidelines +│ ├── template.md # Team's ADR template +│ ├── 0001-use-postgresql.md +│ ├── 0002-caching-strategy.md +│ ├── 0003-mongodb-user-profiles.md # [DEPRECATED] +│ └── 0020-deprecate-mongodb.md # Supersedes 0003 +``` + +### ADR Index (README.md) + +```markdown +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for [Project Name]. + +## Index + +| ADR | Title | Status | Date | +|-----|-------|--------|------| +| [0001](0001-use-postgresql.md) | Use PostgreSQL as Primary Database | Accepted | 2024-01-10 | +| [0002](0002-caching-strategy.md) | Caching Strategy with Redis | Accepted | 2024-01-12 | +| [0003](0003-mongodb-user-profiles.md) | MongoDB for User Profiles | Deprecated | 2023-06-15 | +| [0020](0020-deprecate-mongodb.md) | Deprecate MongoDB | Accepted | 2024-01-15 | + +## Creating a New ADR + +1. Copy `template.md` to `NNNN-title-with-dashes.md` +2. Fill in the template +3. Submit PR for review +4. Update this index after approval + +## ADR Status + +- **Proposed**: Under discussion +- **Accepted**: Decision made, implementing +- **Deprecated**: No longer relevant +- **Superseded**: Replaced by another ADR +- **Rejected**: Considered but not adopted +``` + +### Automation (adr-tools) + +```bash +# Install adr-tools +brew install adr-tools + +# Initialize ADR directory +adr init docs/adr + +# Create new ADR +adr new "Use PostgreSQL as Primary Database" + +# Supersede an ADR +adr new -s 3 "Deprecate MongoDB in Favor of PostgreSQL" + +# Generate table of contents +adr generate toc > docs/adr/README.md + +# Link related ADRs +adr link 2 "Complements" 1 "Is complemented by" +``` + +## Review Process + +```markdown +## ADR Review Checklist + +### Before Submission +- [ ] Context clearly explains the problem +- [ ] All viable options considered +- [ ] Pros/cons balanced and honest +- [ ] Consequences (positive and negative) documented +- [ ] Related ADRs linked + +### During Review +- [ ] At least 2 senior engineers reviewed +- [ ] Affected teams consulted +- [ ] Security implications considered +- [ ] Cost implications documented +- [ ] Reversibility assessed + +### After Acceptance +- [ ] ADR index updated +- [ ] Team notified +- [ ] Implementation tickets created +- [ ] Related documentation updated +``` + +## Best Practices + +### Do's +- **Write ADRs early** - Before implementation starts +- **Keep them short** - 1-2 pages maximum +- **Be honest about trade-offs** - Include real cons +- **Link related decisions** - Build decision graph +- **Update status** - Deprecate when superseded + +### Don'ts +- **Don't change accepted ADRs** - Write new ones to supersede +- **Don't skip context** - Future readers need background +- **Don't hide failures** - Rejected decisions are valuable +- **Don't be vague** - Specific decisions, specific consequences +- **Don't forget implementation** - ADR without action is waste + +## Resources + +- [Documenting Architecture Decisions (Michael Nygard)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) +- [MADR Template](https://adr.github.io/madr/) +- [ADR GitHub Organization](https://adr.github.io/) +- [adr-tools](https://github.com/npryce/adr-tools) diff --git a/plugins/documentation-generation/skills/changelog-automation/SKILL.md b/plugins/documentation-generation/skills/changelog-automation/SKILL.md new file mode 100644 index 0000000..0e91d03 --- /dev/null +++ b/plugins/documentation-generation/skills/changelog-automation/SKILL.md @@ -0,0 +1,552 @@ +--- +name: changelog-automation +description: Automate changelog generation from commits, PRs, and releases following Keep a Changelog format. Use when setting up release workflows, generating release notes, or standardizing commit conventions. +--- + +# Changelog Automation + +Patterns and tools for automating changelog generation, release notes, and version management following industry standards. + +## When to Use This Skill + +- Setting up automated changelog generation +- Implementing Conventional Commits +- Creating release note workflows +- Standardizing commit message formats +- Generating GitHub/GitLab release notes +- Managing semantic versioning + +## Core Concepts + +### 1. Keep a Changelog Format + +```markdown +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- New feature X + +## [1.2.0] - 2024-01-15 + +### Added +- User profile avatars +- Dark mode support + +### Changed +- Improved loading performance by 40% + +### Deprecated +- Old authentication API (use v2) + +### Removed +- Legacy payment gateway + +### Fixed +- Login timeout issue (#123) + +### Security +- Updated dependencies for CVE-2024-1234 + +[Unreleased]: https://github.com/user/repo/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/user/repo/compare/v1.1.0...v1.2.0 +``` + +### 2. Conventional Commits + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +| Type | Description | Changelog Section | +|------|-------------|-------------------| +| `feat` | New feature | Added | +| `fix` | Bug fix | Fixed | +| `docs` | Documentation | (usually excluded) | +| `style` | Formatting | (usually excluded) | +| `refactor` | Code restructure | Changed | +| `perf` | Performance | Changed | +| `test` | Tests | (usually excluded) | +| `chore` | Maintenance | (usually excluded) | +| `ci` | CI changes | (usually excluded) | +| `build` | Build system | (usually excluded) | +| `revert` | Revert commit | Removed | + +### 3. Semantic Versioning + +``` +MAJOR.MINOR.PATCH + +MAJOR: Breaking changes (feat! or BREAKING CHANGE) +MINOR: New features (feat) +PATCH: Bug fixes (fix) +``` + +## Implementation + +### Method 1: Conventional Changelog (Node.js) + +```bash +# Install tools +npm install -D @commitlint/cli @commitlint/config-conventional +npm install -D husky +npm install -D standard-version +# or +npm install -D semantic-release + +# Setup commitlint +cat > commitlint.config.js << 'EOF' +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'feat', + 'fix', + 'docs', + 'style', + 'refactor', + 'perf', + 'test', + 'chore', + 'ci', + 'build', + 'revert', + ], + ], + 'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']], + 'subject-max-length': [2, 'always', 72], + }, +}; +EOF + +# Setup husky +npx husky init +echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg +``` + +### Method 2: standard-version Configuration + +```javascript +// .versionrc.js +module.exports = { + types: [ + { type: 'feat', section: 'Features' }, + { type: 'fix', section: 'Bug Fixes' }, + { type: 'perf', section: 'Performance Improvements' }, + { type: 'revert', section: 'Reverts' }, + { type: 'docs', section: 'Documentation', hidden: true }, + { type: 'style', section: 'Styles', hidden: true }, + { type: 'chore', section: 'Miscellaneous', hidden: true }, + { type: 'refactor', section: 'Code Refactoring', hidden: true }, + { type: 'test', section: 'Tests', hidden: true }, + { type: 'build', section: 'Build System', hidden: true }, + { type: 'ci', section: 'CI/CD', hidden: true }, + ], + commitUrlFormat: '{{host}}/{{owner}}/{{repository}}/commit/{{hash}}', + compareUrlFormat: '{{host}}/{{owner}}/{{repository}}/compare/{{previousTag}}...{{currentTag}}', + issueUrlFormat: '{{host}}/{{owner}}/{{repository}}/issues/{{id}}', + userUrlFormat: '{{host}}/{{user}}', + releaseCommitMessageFormat: 'chore(release): {{currentTag}}', + scripts: { + prebump: 'echo "Running prebump"', + postbump: 'echo "Running postbump"', + prechangelog: 'echo "Running prechangelog"', + postchangelog: 'echo "Running postchangelog"', + }, +}; +``` + +```json +// package.json scripts +{ + "scripts": { + "release": "standard-version", + "release:minor": "standard-version --release-as minor", + "release:major": "standard-version --release-as major", + "release:patch": "standard-version --release-as patch", + "release:dry": "standard-version --dry-run" + } +} +``` + +### Method 3: semantic-release (Full Automation) + +```javascript +// release.config.js +module.exports = { + branches: [ + 'main', + { name: 'beta', prerelease: true }, + { name: 'alpha', prerelease: true }, + ], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + [ + '@semantic-release/changelog', + { + changelogFile: 'CHANGELOG.md', + }, + ], + [ + '@semantic-release/npm', + { + npmPublish: true, + }, + ], + [ + '@semantic-release/github', + { + assets: ['dist/**/*.js', 'dist/**/*.css'], + }, + ], + [ + '@semantic-release/git', + { + assets: ['CHANGELOG.md', 'package.json'], + message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }, + ], + ], +}; +``` + +### Method 4: GitHub Actions Workflow + +```yaml +# .github/workflows/release.yml +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + pull-requests: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - run: npm ci + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Run semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release + + # Alternative: manual release with standard-version + manual-release: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - run: npm ci + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump version and generate changelog + run: npx standard-version --release-as ${{ inputs.release_type }} + + - name: Push changes + run: git push --follow-tags origin main + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.version.outputs.tag }} + body_path: RELEASE_NOTES.md + generate_release_notes: true +``` + +### Method 5: git-cliff (Rust-based, Fast) + +```toml +# cliff.toml +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +""" +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.github.pr_number %} ([#{{ commit.github.pr_number }}](https://github.com/owner/repo/pull/{{ commit.github.pr_number }})){% endif %}\ + {% endfor %} +{% endfor %} +""" +footer = """ +{% for release in releases -%} + {% if release.version -%} + {% if release.previous.version -%} + [{{ release.version | trim_start_matches(pat="v") }}]: \ + https://github.com/owner/repo/compare/{{ release.previous.version }}...{{ release.version }} + {% endif -%} + {% else -%} + [unreleased]: https://github.com/owner/repo/compare/{{ release.previous.version }}...HEAD + {% endif -%} +{% endfor %} +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore", group = "Miscellaneous" }, +] +filter_commits = false +tag_pattern = "v[0-9]*" +skip_tags = "" +ignore_tags = "" +topo_order = false +sort_commits = "oldest" + +[github] +owner = "owner" +repo = "repo" +``` + +```bash +# Generate changelog +git cliff -o CHANGELOG.md + +# Generate for specific range +git cliff v1.0.0..v2.0.0 -o RELEASE_NOTES.md + +# Preview without writing +git cliff --unreleased --dry-run +``` + +### Method 6: Python (commitizen) + +```toml +# pyproject.toml +[tool.commitizen] +name = "cz_conventional_commits" +version = "1.0.0" +version_files = [ + "pyproject.toml:version", + "src/__init__.py:__version__", +] +tag_format = "v$version" +update_changelog_on_bump = true +changelog_incremental = true +changelog_start_rev = "v0.1.0" + +[tool.commitizen.customize] +message_template = "{{change_type}}{% if scope %}({{scope}}){% endif %}: {{message}}" +schema = "(): " +schema_pattern = "^(feat|fix|docs|style|refactor|perf|test|chore)(\\(\\w+\\))?:\\s.*" +bump_pattern = "^(feat|fix|perf|refactor)" +bump_map = {"feat" = "MINOR", "fix" = "PATCH", "perf" = "PATCH", "refactor" = "PATCH"} +``` + +```bash +# Install +pip install commitizen + +# Create commit interactively +cz commit + +# Bump version and update changelog +cz bump --changelog + +# Check commits +cz check --rev-range HEAD~5..HEAD +``` + +## Release Notes Templates + +### GitHub Release Template + +```markdown +## What's Changed + +### 🚀 Features +{{ range .Features }} +- {{ .Title }} by @{{ .Author }} in #{{ .PR }} +{{ end }} + +### 🐛 Bug Fixes +{{ range .Fixes }} +- {{ .Title }} by @{{ .Author }} in #{{ .PR }} +{{ end }} + +### 📚 Documentation +{{ range .Docs }} +- {{ .Title }} by @{{ .Author }} in #{{ .PR }} +{{ end }} + +### 🔧 Maintenance +{{ range .Chores }} +- {{ .Title }} by @{{ .Author }} in #{{ .PR }} +{{ end }} + +## New Contributors +{{ range .NewContributors }} +- @{{ .Username }} made their first contribution in #{{ .PR }} +{{ end }} + +**Full Changelog**: https://github.com/owner/repo/compare/v{{ .Previous }}...v{{ .Current }} +``` + +### Internal Release Notes + +```markdown +# Release v2.1.0 - January 15, 2024 + +## Summary +This release introduces dark mode support and improves checkout performance +by 40%. It also includes important security updates. + +## Highlights + +### 🌙 Dark Mode +Users can now switch to dark mode from settings. The preference is +automatically saved and synced across devices. + +### ⚡ Performance +- Checkout flow is 40% faster +- Reduced bundle size by 15% + +## Breaking Changes +None in this release. + +## Upgrade Guide +No special steps required. Standard deployment process applies. + +## Known Issues +- Dark mode may flicker on initial load (fix scheduled for v2.1.1) + +## Dependencies Updated +| Package | From | To | Reason | +|---------|------|-----|--------| +| react | 18.2.0 | 18.3.0 | Performance improvements | +| lodash | 4.17.20 | 4.17.21 | Security patch | +``` + +## Commit Message Examples + +```bash +# Feature with scope +feat(auth): add OAuth2 support for Google login + +# Bug fix with issue reference +fix(checkout): resolve race condition in payment processing + +Closes #123 + +# Breaking change +feat(api)!: change user endpoint response format + +BREAKING CHANGE: The user endpoint now returns `userId` instead of `id`. +Migration guide: Update all API consumers to use the new field name. + +# Multiple paragraphs +fix(database): handle connection timeouts gracefully + +Previously, connection timeouts would cause the entire request to fail +without retry. This change implements exponential backoff with up to +3 retries before failing. + +The timeout threshold has been increased from 5s to 10s based on p99 +latency analysis. + +Fixes #456 +Reviewed-by: @alice +``` + +## Best Practices + +### Do's +- **Follow Conventional Commits** - Enables automation +- **Write clear messages** - Future you will thank you +- **Reference issues** - Link commits to tickets +- **Use scopes consistently** - Define team conventions +- **Automate releases** - Reduce manual errors + +### Don'ts +- **Don't mix changes** - One logical change per commit +- **Don't skip validation** - Use commitlint +- **Don't manual edit** - Generated changelogs only +- **Don't forget breaking changes** - Mark with `!` or footer +- **Don't ignore CI** - Validate commits in pipeline + +## Resources + +- [Keep a Changelog](https://keepachangelog.com/) +- [Conventional Commits](https://www.conventionalcommits.org/) +- [Semantic Versioning](https://semver.org/) +- [semantic-release](https://semantic-release.gitbook.io/) +- [git-cliff](https://git-cliff.org/) diff --git a/plugins/documentation-generation/skills/openapi-spec-generation/SKILL.md b/plugins/documentation-generation/skills/openapi-spec-generation/SKILL.md new file mode 100644 index 0000000..b8fb478 --- /dev/null +++ b/plugins/documentation-generation/skills/openapi-spec-generation/SKILL.md @@ -0,0 +1,1028 @@ +--- +name: openapi-spec-generation +description: Generate and maintain OpenAPI 3.1 specifications from code, design-first specs, and validation patterns. Use when creating API documentation, generating SDKs, or ensuring API contract compliance. +--- + +# OpenAPI Spec Generation + +Comprehensive patterns for creating, maintaining, and validating OpenAPI 3.1 specifications for RESTful APIs. + +## When to Use This Skill + +- Creating API documentation from scratch +- Generating OpenAPI specs from existing code +- Designing API contracts (design-first approach) +- Validating API implementations against specs +- Generating client SDKs from specs +- Setting up API documentation portals + +## Core Concepts + +### 1. OpenAPI 3.1 Structure + +```yaml +openapi: 3.1.0 +info: + title: API Title + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /resources: + get: ... +components: + schemas: ... + securitySchemes: ... +``` + +### 2. Design Approaches + +| Approach | Description | Best For | +|----------|-------------|----------| +| **Design-First** | Write spec before code | New APIs, contracts | +| **Code-First** | Generate spec from code | Existing APIs | +| **Hybrid** | Annotate code, generate spec | Evolving APIs | + +## Templates + +### Template 1: Complete API Specification + +```yaml +openapi: 3.1.0 +info: + title: User Management API + description: | + API for managing users and their profiles. + + ## Authentication + All endpoints require Bearer token authentication. + + ## Rate Limiting + - 1000 requests per minute for standard tier + - 10000 requests per minute for enterprise tier + version: 2.0.0 + contact: + name: API Support + email: api-support@example.com + url: https://docs.example.com + license: + name: MIT + url: https://opensource.org/licenses/MIT + +servers: + - url: https://api.example.com/v2 + description: Production + - url: https://staging-api.example.com/v2 + description: Staging + - url: http://localhost:3000/v2 + description: Local development + +tags: + - name: Users + description: User management operations + - name: Profiles + description: User profile operations + - name: Admin + description: Administrative operations + +paths: + /users: + get: + operationId: listUsers + summary: List all users + description: Returns a paginated list of users with optional filtering. + tags: + - Users + parameters: + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/LimitParam' + - name: status + in: query + description: Filter by user status + schema: + $ref: '#/components/schemas/UserStatus' + - name: search + in: query + description: Search by name or email + schema: + type: string + minLength: 2 + maxLength: 100 + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/UserListResponse' + examples: + default: + $ref: '#/components/examples/UserListExample' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + security: + - bearerAuth: [] + + post: + operationId: createUser + summary: Create a new user + description: Creates a new user account and sends welcome email. + tags: + - Users + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserRequest' + examples: + standard: + summary: Standard user + value: + email: user@example.com + name: John Doe + role: user + admin: + summary: Admin user + value: + email: admin@example.com + name: Admin User + role: admin + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + headers: + Location: + description: URL of created user + schema: + type: string + format: uri + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: Email already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + security: + - bearerAuth: [] + + /users/{userId}: + parameters: + - $ref: '#/components/parameters/UserIdParam' + + get: + operationId: getUser + summary: Get user by ID + tags: + - Users + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + $ref: '#/components/responses/NotFound' + security: + - bearerAuth: [] + + patch: + operationId: updateUser + summary: Update user + tags: + - Users + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + responses: + '200': + description: User updated + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + security: + - bearerAuth: [] + + delete: + operationId: deleteUser + summary: Delete user + tags: + - Users + - Admin + responses: + '204': + description: User deleted + '404': + $ref: '#/components/responses/NotFound' + security: + - bearerAuth: [] + - apiKey: [] + +components: + schemas: + User: + type: object + required: + - id + - email + - name + - status + - createdAt + properties: + id: + type: string + format: uuid + readOnly: true + description: Unique user identifier + email: + type: string + format: email + description: User email address + name: + type: string + minLength: 1 + maxLength: 100 + description: User display name + status: + $ref: '#/components/schemas/UserStatus' + role: + type: string + enum: [user, moderator, admin] + default: user + avatar: + type: string + format: uri + nullable: true + metadata: + type: object + additionalProperties: true + description: Custom metadata + createdAt: + type: string + format: date-time + readOnly: true + updatedAt: + type: string + format: date-time + readOnly: true + + UserStatus: + type: string + enum: [active, inactive, suspended, pending] + description: User account status + + CreateUserRequest: + type: object + required: + - email + - name + properties: + email: + type: string + format: email + name: + type: string + minLength: 1 + maxLength: 100 + role: + type: string + enum: [user, moderator, admin] + default: user + metadata: + type: object + additionalProperties: true + + UpdateUserRequest: + type: object + minProperties: 1 + properties: + name: + type: string + minLength: 1 + maxLength: 100 + status: + $ref: '#/components/schemas/UserStatus' + role: + type: string + enum: [user, moderator, admin] + metadata: + type: object + additionalProperties: true + + UserListResponse: + type: object + required: + - data + - pagination + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + required: + - page + - limit + - total + - totalPages + properties: + page: + type: integer + minimum: 1 + limit: + type: integer + minimum: 1 + maximum: 100 + total: + type: integer + minimum: 0 + totalPages: + type: integer + minimum: 0 + hasNext: + type: boolean + hasPrev: + type: boolean + + Error: + type: object + required: + - code + - message + properties: + code: + type: string + description: Error code for programmatic handling + message: + type: string + description: Human-readable error message + details: + type: array + items: + type: object + properties: + field: + type: string + message: + type: string + requestId: + type: string + description: Request ID for support + + parameters: + UserIdParam: + name: userId + in: path + required: true + description: User ID + schema: + type: string + format: uuid + + PageParam: + name: page + in: query + description: Page number (1-based) + schema: + type: integer + minimum: 1 + default: 1 + + LimitParam: + name: limit + in: query + description: Items per page + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + + responses: + BadRequest: + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + code: VALIDATION_ERROR + message: Invalid request parameters + details: + - field: email + message: Must be a valid email address + + Unauthorized: + description: Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + code: UNAUTHORIZED + message: Authentication required + + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + code: NOT_FOUND + message: User not found + + RateLimited: + description: Too many requests + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + headers: + Retry-After: + description: Seconds until rate limit resets + schema: + type: integer + X-RateLimit-Limit: + description: Request limit per window + schema: + type: integer + X-RateLimit-Remaining: + description: Remaining requests in window + schema: + type: integer + + examples: + UserListExample: + value: + data: + - id: "550e8400-e29b-41d4-a716-446655440000" + email: "john@example.com" + name: "John Doe" + status: "active" + role: "user" + createdAt: "2024-01-15T10:30:00Z" + pagination: + page: 1 + limit: 20 + total: 1 + totalPages: 1 + hasNext: false + hasPrev: false + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT token from /auth/login + + apiKey: + type: apiKey + in: header + name: X-API-Key + description: API key for service-to-service calls + +security: + - bearerAuth: [] +``` + +### Template 2: Code-First Generation (Python/FastAPI) + +```python +# FastAPI with automatic OpenAPI generation +from fastapi import FastAPI, HTTPException, Query, Path, Depends +from pydantic import BaseModel, Field, EmailStr +from typing import Optional, List +from datetime import datetime +from uuid import UUID +from enum import Enum + +app = FastAPI( + title="User Management API", + description="API for managing users and profiles", + version="2.0.0", + openapi_tags=[ + {"name": "Users", "description": "User operations"}, + {"name": "Profiles", "description": "Profile operations"}, + ], + servers=[ + {"url": "https://api.example.com/v2", "description": "Production"}, + {"url": "http://localhost:8000", "description": "Development"}, + ], +) + +# Enums +class UserStatus(str, Enum): + active = "active" + inactive = "inactive" + suspended = "suspended" + pending = "pending" + +class UserRole(str, Enum): + user = "user" + moderator = "moderator" + admin = "admin" + +# Models +class UserBase(BaseModel): + email: EmailStr = Field(..., description="User email address") + name: str = Field(..., min_length=1, max_length=100, description="Display name") + +class UserCreate(UserBase): + role: UserRole = Field(default=UserRole.user) + metadata: Optional[dict] = Field(default=None, description="Custom metadata") + + model_config = { + "json_schema_extra": { + "examples": [ + { + "email": "user@example.com", + "name": "John Doe", + "role": "user" + } + ] + } + } + +class UserUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=100) + status: Optional[UserStatus] = None + role: Optional[UserRole] = None + metadata: Optional[dict] = None + +class User(UserBase): + id: UUID = Field(..., description="Unique identifier") + status: UserStatus + role: UserRole + avatar: Optional[str] = Field(None, description="Avatar URL") + metadata: Optional[dict] = None + created_at: datetime = Field(..., alias="createdAt") + updated_at: Optional[datetime] = Field(None, alias="updatedAt") + + model_config = {"populate_by_name": True} + +class Pagination(BaseModel): + page: int = Field(..., ge=1) + limit: int = Field(..., ge=1, le=100) + total: int = Field(..., ge=0) + total_pages: int = Field(..., ge=0, alias="totalPages") + has_next: bool = Field(..., alias="hasNext") + has_prev: bool = Field(..., alias="hasPrev") + +class UserListResponse(BaseModel): + data: List[User] + pagination: Pagination + +class ErrorDetail(BaseModel): + field: str + message: str + +class ErrorResponse(BaseModel): + code: str = Field(..., description="Error code") + message: str = Field(..., description="Error message") + details: Optional[List[ErrorDetail]] = None + request_id: Optional[str] = Field(None, alias="requestId") + +# Endpoints +@app.get( + "/users", + response_model=UserListResponse, + tags=["Users"], + summary="List all users", + description="Returns a paginated list of users with optional filtering.", + responses={ + 400: {"model": ErrorResponse, "description": "Invalid request"}, + 401: {"model": ErrorResponse, "description": "Unauthorized"}, + }, +) +async def list_users( + page: int = Query(1, ge=1, description="Page number"), + limit: int = Query(20, ge=1, le=100, description="Items per page"), + status: Optional[UserStatus] = Query(None, description="Filter by status"), + search: Optional[str] = Query(None, min_length=2, max_length=100), +): + """ + List users with pagination and filtering. + + - **page**: Page number (1-based) + - **limit**: Number of items per page (max 100) + - **status**: Filter by user status + - **search**: Search by name or email + """ + # Implementation + pass + +@app.post( + "/users", + response_model=User, + status_code=201, + tags=["Users"], + summary="Create a new user", + responses={ + 400: {"model": ErrorResponse}, + 409: {"model": ErrorResponse, "description": "Email already exists"}, + }, +) +async def create_user(user: UserCreate): + """Create a new user and send welcome email.""" + pass + +@app.get( + "/users/{user_id}", + response_model=User, + tags=["Users"], + summary="Get user by ID", + responses={404: {"model": ErrorResponse}}, +) +async def get_user( + user_id: UUID = Path(..., description="User ID"), +): + """Retrieve a specific user by their ID.""" + pass + +@app.patch( + "/users/{user_id}", + response_model=User, + tags=["Users"], + summary="Update user", + responses={ + 400: {"model": ErrorResponse}, + 404: {"model": ErrorResponse}, + }, +) +async def update_user( + user_id: UUID = Path(..., description="User ID"), + user: UserUpdate = ..., +): + """Update user attributes.""" + pass + +@app.delete( + "/users/{user_id}", + status_code=204, + tags=["Users", "Admin"], + summary="Delete user", + responses={404: {"model": ErrorResponse}}, +) +async def delete_user( + user_id: UUID = Path(..., description="User ID"), +): + """Permanently delete a user.""" + pass + +# Export OpenAPI spec +if __name__ == "__main__": + import json + print(json.dumps(app.openapi(), indent=2)) +``` + +### Template 3: Code-First (TypeScript/Express with tsoa) + +```typescript +// tsoa generates OpenAPI from TypeScript decorators + +import { + Controller, + Get, + Post, + Patch, + Delete, + Route, + Path, + Query, + Body, + Response, + SuccessResponse, + Tags, + Security, + Example, +} from "tsoa"; + +// Models +interface User { + /** Unique identifier */ + id: string; + /** User email address */ + email: string; + /** Display name */ + name: string; + status: UserStatus; + role: UserRole; + /** Avatar URL */ + avatar?: string; + /** Custom metadata */ + metadata?: Record; + createdAt: Date; + updatedAt?: Date; +} + +enum UserStatus { + Active = "active", + Inactive = "inactive", + Suspended = "suspended", + Pending = "pending", +} + +enum UserRole { + User = "user", + Moderator = "moderator", + Admin = "admin", +} + +interface CreateUserRequest { + email: string; + name: string; + role?: UserRole; + metadata?: Record; +} + +interface UpdateUserRequest { + name?: string; + status?: UserStatus; + role?: UserRole; + metadata?: Record; +} + +interface Pagination { + page: number; + limit: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + +interface UserListResponse { + data: User[]; + pagination: Pagination; +} + +interface ErrorResponse { + code: string; + message: string; + details?: { field: string; message: string }[]; + requestId?: string; +} + +@Route("users") +@Tags("Users") +export class UsersController extends Controller { + /** + * List all users with pagination and filtering + * @param page Page number (1-based) + * @param limit Items per page (max 100) + * @param status Filter by user status + * @param search Search by name or email + */ + @Get() + @Security("bearerAuth") + @Response(400, "Invalid request") + @Response(401, "Unauthorized") + @Example({ + data: [ + { + id: "550e8400-e29b-41d4-a716-446655440000", + email: "john@example.com", + name: "John Doe", + status: UserStatus.Active, + role: UserRole.User, + createdAt: new Date("2024-01-15T10:30:00Z"), + }, + ], + pagination: { + page: 1, + limit: 20, + total: 1, + totalPages: 1, + hasNext: false, + hasPrev: false, + }, + }) + public async listUsers( + @Query() page: number = 1, + @Query() limit: number = 20, + @Query() status?: UserStatus, + @Query() search?: string + ): Promise { + // Implementation + throw new Error("Not implemented"); + } + + /** + * Create a new user + */ + @Post() + @Security("bearerAuth") + @SuccessResponse(201, "Created") + @Response(400, "Invalid request") + @Response(409, "Email already exists") + public async createUser( + @Body() body: CreateUserRequest + ): Promise { + this.setStatus(201); + throw new Error("Not implemented"); + } + + /** + * Get user by ID + * @param userId User ID + */ + @Get("{userId}") + @Security("bearerAuth") + @Response(404, "User not found") + public async getUser( + @Path() userId: string + ): Promise { + throw new Error("Not implemented"); + } + + /** + * Update user attributes + * @param userId User ID + */ + @Patch("{userId}") + @Security("bearerAuth") + @Response(400, "Invalid request") + @Response(404, "User not found") + public async updateUser( + @Path() userId: string, + @Body() body: UpdateUserRequest + ): Promise { + throw new Error("Not implemented"); + } + + /** + * Delete user + * @param userId User ID + */ + @Delete("{userId}") + @Tags("Users", "Admin") + @Security("bearerAuth") + @SuccessResponse(204, "Deleted") + @Response(404, "User not found") + public async deleteUser( + @Path() userId: string + ): Promise { + this.setStatus(204); + } +} +``` + +### Template 4: Validation & Linting + +```bash +# Install validation tools +npm install -g @stoplight/spectral-cli +npm install -g @redocly/cli + +# Spectral ruleset (.spectral.yaml) +cat > .spectral.yaml << 'EOF' +extends: ["spectral:oas", "spectral:asyncapi"] + +rules: + # Enforce operation IDs + operation-operationId: error + + # Require descriptions + operation-description: warn + info-description: error + + # Naming conventions + operation-operationId-valid-in-url: true + + # Security + operation-security-defined: error + + # Response codes + operation-success-response: error + + # Custom rules + path-params-snake-case: + description: Path parameters should be snake_case + severity: warn + given: "$.paths[*].parameters[?(@.in == 'path')].name" + then: + function: pattern + functionOptions: + match: "^[a-z][a-z0-9_]*$" + + schema-properties-camelCase: + description: Schema properties should be camelCase + severity: warn + given: "$.components.schemas[*].properties[*]~" + then: + function: casing + functionOptions: + type: camel +EOF + +# Run Spectral +spectral lint openapi.yaml + +# Redocly config (redocly.yaml) +cat > redocly.yaml << 'EOF' +extends: + - recommended + +rules: + no-invalid-media-type-examples: error + no-invalid-schema-examples: error + operation-4xx-response: warn + request-mime-type: + severity: error + allowedValues: + - application/json + response-mime-type: + severity: error + allowedValues: + - application/json + - application/problem+json + +theme: + openapi: + generateCodeSamples: + languages: + - lang: curl + - lang: python + - lang: javascript +EOF + +# Run Redocly +redocly lint openapi.yaml +redocly bundle openapi.yaml -o bundled.yaml +redocly preview-docs openapi.yaml +``` + +## SDK Generation + +```bash +# OpenAPI Generator +npm install -g @openapitools/openapi-generator-cli + +# Generate TypeScript client +openapi-generator-cli generate \ + -i openapi.yaml \ + -g typescript-fetch \ + -o ./generated/typescript-client \ + --additional-properties=supportsES6=true,npmName=@myorg/api-client + +# Generate Python client +openapi-generator-cli generate \ + -i openapi.yaml \ + -g python \ + -o ./generated/python-client \ + --additional-properties=packageName=api_client + +# Generate Go client +openapi-generator-cli generate \ + -i openapi.yaml \ + -g go \ + -o ./generated/go-client +``` + +## Best Practices + +### Do's +- **Use $ref** - Reuse schemas, parameters, responses +- **Add examples** - Real-world values help consumers +- **Document errors** - All possible error codes +- **Version your API** - In URL or header +- **Use semantic versioning** - For spec changes + +### Don'ts +- **Don't use generic descriptions** - Be specific +- **Don't skip security** - Define all schemes +- **Don't forget nullable** - Be explicit about null +- **Don't mix styles** - Consistent naming throughout +- **Don't hardcode URLs** - Use server variables + +## Resources + +- [OpenAPI 3.1 Specification](https://spec.openapis.org/oas/v3.1.0) +- [Swagger Editor](https://editor.swagger.io/) +- [Redocly](https://redocly.com/) +- [Spectral](https://stoplight.io/open-source/spectral) diff --git a/plugins/frontend-mobile-development/skills/nextjs-app-router-patterns/SKILL.md b/plugins/frontend-mobile-development/skills/nextjs-app-router-patterns/SKILL.md new file mode 100644 index 0000000..9b86cdd --- /dev/null +++ b/plugins/frontend-mobile-development/skills/nextjs-app-router-patterns/SKILL.md @@ -0,0 +1,544 @@ +--- +name: nextjs-app-router-patterns +description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components. +--- + +# Next.js App Router Patterns + +Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development. + +## When to Use This Skill + +- Building new Next.js applications with App Router +- Migrating from Pages Router to App Router +- Implementing Server Components and streaming +- Setting up parallel and intercepting routes +- Optimizing data fetching and caching +- Building full-stack features with Server Actions + +## Core Concepts + +### 1. Rendering Modes + +| Mode | Where | When to Use | +|------|-------|-------------| +| **Server Components** | Server only | Data fetching, heavy computation, secrets | +| **Client Components** | Browser | Interactivity, hooks, browser APIs | +| **Static** | Build time | Content that rarely changes | +| **Dynamic** | Request time | Personalized or real-time data | +| **Streaming** | Progressive | Large pages, slow data sources | + +### 2. File Conventions + +``` +app/ +├── layout.tsx # Shared UI wrapper +├── page.tsx # Route UI +├── loading.tsx # Loading UI (Suspense) +├── error.tsx # Error boundary +├── not-found.tsx # 404 UI +├── route.ts # API endpoint +├── template.tsx # Re-mounted layout +├── default.tsx # Parallel route fallback +└── opengraph-image.tsx # OG image generation +``` + +## Quick Start + +```typescript +// app/layout.tsx +import { Inter } from 'next/font/google' +import { Providers } from './providers' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata = { + title: { default: 'My App', template: '%s | My App' }, + description: 'Built with Next.js App Router', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} + +// app/page.tsx - Server Component by default +async function getProducts() { + const res = await fetch('https://api.example.com/products', { + next: { revalidate: 3600 }, // ISR: revalidate every hour + }) + return res.json() +} + +export default async function HomePage() { + const products = await getProducts() + + return ( +
+

Products

+ +
+ ) +} +``` + +## Patterns + +### Pattern 1: Server Components with Data Fetching + +```typescript +// app/products/page.tsx +import { Suspense } from 'react' +import { ProductList, ProductListSkeleton } from '@/components/products' +import { FilterSidebar } from '@/components/filters' + +interface SearchParams { + category?: string + sort?: 'price' | 'name' | 'date' + page?: string +} + +export default async function ProductsPage({ + searchParams, +}: { + searchParams: Promise +}) { + const params = await searchParams + + return ( +
+ + } + > + + +
+ ) +} + +// components/products/ProductList.tsx - Server Component +async function getProducts(filters: ProductFilters) { + const res = await fetch( + `${process.env.API_URL}/products?${new URLSearchParams(filters)}`, + { next: { tags: ['products'] } } + ) + if (!res.ok) throw new Error('Failed to fetch products') + return res.json() +} + +export async function ProductList({ category, sort, page }: ProductFilters) { + const { products, totalPages } = await getProducts({ category, sort, page }) + + return ( +
+
+ {products.map((product) => ( + + ))} +
+ +
+ ) +} +``` + +### Pattern 2: Client Components with 'use client' + +```typescript +// components/products/AddToCartButton.tsx +'use client' + +import { useState, useTransition } from 'react' +import { addToCart } from '@/app/actions/cart' + +export function AddToCartButton({ productId }: { productId: string }) { + const [isPending, startTransition] = useTransition() + const [error, setError] = useState(null) + + const handleClick = () => { + setError(null) + startTransition(async () => { + const result = await addToCart(productId) + if (result.error) { + setError(result.error) + } + }) + } + + return ( +
+ + {error &&

{error}

} +
+ ) +} +``` + +### Pattern 3: Server Actions + +```typescript +// app/actions/cart.ts +'use server' + +import { revalidateTag } from 'next/cache' +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' + +export async function addToCart(productId: string) { + const cookieStore = await cookies() + const sessionId = cookieStore.get('session')?.value + + if (!sessionId) { + redirect('/login') + } + + try { + await db.cart.upsert({ + where: { sessionId_productId: { sessionId, productId } }, + update: { quantity: { increment: 1 } }, + create: { sessionId, productId, quantity: 1 }, + }) + + revalidateTag('cart') + return { success: true } + } catch (error) { + return { error: 'Failed to add item to cart' } + } +} + +export async function checkout(formData: FormData) { + const address = formData.get('address') as string + const payment = formData.get('payment') as string + + // Validate + if (!address || !payment) { + return { error: 'Missing required fields' } + } + + // Process order + const order = await processOrder({ address, payment }) + + // Redirect to confirmation + redirect(`/orders/${order.id}/confirmation`) +} +``` + +### Pattern 4: Parallel Routes + +```typescript +// app/dashboard/layout.tsx +export default function DashboardLayout({ + children, + analytics, + team, +}: { + children: React.ReactNode + analytics: React.ReactNode + team: React.ReactNode +}) { + return ( +
+
{children}
+ + +
+ ) +} + +// app/dashboard/@analytics/page.tsx +export default async function AnalyticsSlot() { + const stats = await getAnalytics() + return +} + +// app/dashboard/@analytics/loading.tsx +export default function AnalyticsLoading() { + return +} + +// app/dashboard/@team/page.tsx +export default async function TeamSlot() { + const members = await getTeamMembers() + return +} +``` + +### Pattern 5: Intercepting Routes (Modal Pattern) + +```typescript +// File structure for photo modal +// app/ +// ├── @modal/ +// │ ├── (.)photos/[id]/page.tsx # Intercept +// │ └── default.tsx +// ├── photos/ +// │ └── [id]/page.tsx # Full page +// └── layout.tsx + +// app/@modal/(.)photos/[id]/page.tsx +import { Modal } from '@/components/Modal' +import { PhotoDetail } from '@/components/PhotoDetail' + +export default async function PhotoModal({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + const photo = await getPhoto(id) + + return ( + + + + ) +} + +// app/photos/[id]/page.tsx - Full page version +export default async function PhotoPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + const photo = await getPhoto(id) + + return ( +
+ + +
+ ) +} + +// app/layout.tsx +export default function RootLayout({ + children, + modal, +}: { + children: React.ReactNode + modal: React.ReactNode +}) { + return ( + + + {children} + {modal} + + + ) +} +``` + +### Pattern 6: Streaming with Suspense + +```typescript +// app/product/[id]/page.tsx +import { Suspense } from 'react' + +export default async function ProductPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + + // This data loads first (blocking) + const product = await getProduct(id) + + return ( +
+ {/* Immediate render */} + + + {/* Stream in reviews */} + }> + + + + {/* Stream in recommendations */} + }> + + +
+ ) +} + +// These components fetch their own data +async function Reviews({ productId }: { productId: string }) { + const reviews = await getReviews(productId) // Slow API + return +} + +async function Recommendations({ productId }: { productId: string }) { + const products = await getRecommendations(productId) // ML-based, slow + return +} +``` + +### Pattern 7: Route Handlers (API Routes) + +```typescript +// app/api/products/route.ts +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const category = searchParams.get('category') + + const products = await db.product.findMany({ + where: category ? { category } : undefined, + take: 20, + }) + + return NextResponse.json(products) +} + +export async function POST(request: NextRequest) { + const body = await request.json() + + const product = await db.product.create({ + data: body, + }) + + return NextResponse.json(product, { status: 201 }) +} + +// app/api/products/[id]/route.ts +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const product = await db.product.findUnique({ where: { id } }) + + if (!product) { + return NextResponse.json( + { error: 'Product not found' }, + { status: 404 } + ) + } + + return NextResponse.json(product) +} +``` + +### Pattern 8: Metadata and SEO + +```typescript +// app/products/[slug]/page.tsx +import { Metadata } from 'next' +import { notFound } from 'next/navigation' + +type Props = { + params: Promise<{ slug: string }> +} + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + const product = await getProduct(slug) + + if (!product) return {} + + return { + title: product.name, + description: product.description, + openGraph: { + title: product.name, + description: product.description, + images: [{ url: product.image, width: 1200, height: 630 }], + }, + twitter: { + card: 'summary_large_image', + title: product.name, + description: product.description, + images: [product.image], + }, + } +} + +export async function generateStaticParams() { + const products = await db.product.findMany({ select: { slug: true } }) + return products.map((p) => ({ slug: p.slug })) +} + +export default async function ProductPage({ params }: Props) { + const { slug } = await params + const product = await getProduct(slug) + + if (!product) notFound() + + return +} +``` + +## Caching Strategies + +### Data Cache + +```typescript +// No cache (always fresh) +fetch(url, { cache: 'no-store' }) + +// Cache forever (static) +fetch(url, { cache: 'force-cache' }) + +// ISR - revalidate after 60 seconds +fetch(url, { next: { revalidate: 60 } }) + +// Tag-based invalidation +fetch(url, { next: { tags: ['products'] } }) + +// Invalidate via Server Action +'use server' +import { revalidateTag, revalidatePath } from 'next/cache' + +export async function updateProduct(id: string, data: ProductData) { + await db.product.update({ where: { id }, data }) + revalidateTag('products') + revalidatePath('/products') +} +``` + +## Best Practices + +### Do's +- **Start with Server Components** - Add 'use client' only when needed +- **Colocate data fetching** - Fetch data where it's used +- **Use Suspense boundaries** - Enable streaming for slow data +- **Leverage parallel routes** - Independent loading states +- **Use Server Actions** - For mutations with progressive enhancement + +### Don'ts +- **Don't pass serializable data** - Server → Client boundary limitations +- **Don't use hooks in Server Components** - No useState, useEffect +- **Don't fetch in Client Components** - Use Server Components or React Query +- **Don't over-nest layouts** - Each layout adds to the component tree +- **Don't ignore loading states** - Always provide loading.tsx or Suspense + +## Resources + +- [Next.js App Router Documentation](https://nextjs.org/docs/app) +- [Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) +- [Vercel Templates](https://vercel.com/templates/next.js) diff --git a/plugins/frontend-mobile-development/skills/react-native-architecture/SKILL.md b/plugins/frontend-mobile-development/skills/react-native-architecture/SKILL.md new file mode 100644 index 0000000..a0a2fe6 --- /dev/null +++ b/plugins/frontend-mobile-development/skills/react-native-architecture/SKILL.md @@ -0,0 +1,671 @@ +--- +name: react-native-architecture +description: Build production React Native apps with Expo, navigation, native modules, offline sync, and cross-platform patterns. Use when developing mobile apps, implementing native integrations, or architecting React Native projects. +--- + +# React Native Architecture + +Production-ready patterns for React Native development with Expo, including navigation, state management, native modules, and offline-first architecture. + +## When to Use This Skill + +- Starting a new React Native or Expo project +- Implementing complex navigation patterns +- Integrating native modules and platform APIs +- Building offline-first mobile applications +- Optimizing React Native performance +- Setting up CI/CD for mobile releases + +## Core Concepts + +### 1. Project Structure + +``` +src/ +├── app/ # Expo Router screens +│ ├── (auth)/ # Auth group +│ ├── (tabs)/ # Tab navigation +│ └── _layout.tsx # Root layout +├── components/ +│ ├── ui/ # Reusable UI components +│ └── features/ # Feature-specific components +├── hooks/ # Custom hooks +├── services/ # API and native services +├── stores/ # State management +├── utils/ # Utilities +└── types/ # TypeScript types +``` + +### 2. Expo vs Bare React Native + +| Feature | Expo | Bare RN | +|---------|------|---------| +| Setup complexity | Low | High | +| Native modules | EAS Build | Manual linking | +| OTA updates | Built-in | Manual setup | +| Build service | EAS | Custom CI | +| Custom native code | Config plugins | Direct access | + +## Quick Start + +```bash +# Create new Expo project +npx create-expo-app@latest my-app -t expo-template-blank-typescript + +# Install essential dependencies +npx expo install expo-router expo-status-bar react-native-safe-area-context +npx expo install @react-native-async-storage/async-storage +npx expo install expo-secure-store expo-haptics +``` + +```typescript +// app/_layout.tsx +import { Stack } from 'expo-router' +import { ThemeProvider } from '@/providers/ThemeProvider' +import { QueryProvider } from '@/providers/QueryProvider' + +export default function RootLayout() { + return ( + + + + + + + + + + ) +} +``` + +## Patterns + +### Pattern 1: Expo Router Navigation + +```typescript +// app/(tabs)/_layout.tsx +import { Tabs } from 'expo-router' +import { Home, Search, User, Settings } from 'lucide-react-native' +import { useTheme } from '@/hooks/useTheme' + +export default function TabLayout() { + const { colors } = useTheme() + + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ) +} + +// app/(tabs)/profile/[id].tsx - Dynamic route +import { useLocalSearchParams } from 'expo-router' + +export default function ProfileScreen() { + const { id } = useLocalSearchParams<{ id: string }>() + + return +} + +// Navigation from anywhere +import { router } from 'expo-router' + +// Programmatic navigation +router.push('/profile/123') +router.replace('/login') +router.back() + +// With params +router.push({ + pathname: '/product/[id]', + params: { id: '123', referrer: 'home' }, +}) +``` + +### Pattern 2: Authentication Flow + +```typescript +// providers/AuthProvider.tsx +import { createContext, useContext, useEffect, useState } from 'react' +import { useRouter, useSegments } from 'expo-router' +import * as SecureStore from 'expo-secure-store' + +interface AuthContextType { + user: User | null + isLoading: boolean + signIn: (credentials: Credentials) => Promise + signOut: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const segments = useSegments() + const router = useRouter() + + // Check authentication on mount + useEffect(() => { + checkAuth() + }, []) + + // Protect routes + useEffect(() => { + if (isLoading) return + + const inAuthGroup = segments[0] === '(auth)' + + if (!user && !inAuthGroup) { + router.replace('/login') + } else if (user && inAuthGroup) { + router.replace('/(tabs)') + } + }, [user, segments, isLoading]) + + async function checkAuth() { + try { + const token = await SecureStore.getItemAsync('authToken') + if (token) { + const userData = await api.getUser(token) + setUser(userData) + } + } catch (error) { + await SecureStore.deleteItemAsync('authToken') + } finally { + setIsLoading(false) + } + } + + async function signIn(credentials: Credentials) { + const { token, user } = await api.login(credentials) + await SecureStore.setItemAsync('authToken', token) + setUser(user) + } + + async function signOut() { + await SecureStore.deleteItemAsync('authToken') + setUser(null) + } + + if (isLoading) { + return + } + + return ( + + {children} + + ) +} + +export const useAuth = () => { + const context = useContext(AuthContext) + if (!context) throw new Error('useAuth must be used within AuthProvider') + return context +} +``` + +### Pattern 3: Offline-First with React Query + +```typescript +// providers/QueryProvider.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' +import AsyncStorage from '@react-native-async-storage/async-storage' +import NetInfo from '@react-native-community/netinfo' +import { onlineManager } from '@tanstack/react-query' + +// Sync online status +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(!!state.isConnected) + }) +}) + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 2, + networkMode: 'offlineFirst', + }, + mutations: { + networkMode: 'offlineFirst', + }, + }, +}) + +const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, + key: 'REACT_QUERY_OFFLINE_CACHE', +}) + +export function QueryProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +// hooks/useProducts.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' + +export function useProducts() { + return useQuery({ + queryKey: ['products'], + queryFn: api.getProducts, + // Use stale data while revalidating + placeholderData: (previousData) => previousData, + }) +} + +export function useCreateProduct() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: api.createProduct, + // Optimistic update + onMutate: async (newProduct) => { + await queryClient.cancelQueries({ queryKey: ['products'] }) + const previous = queryClient.getQueryData(['products']) + + queryClient.setQueryData(['products'], (old: Product[]) => [ + ...old, + { ...newProduct, id: 'temp-' + Date.now() }, + ]) + + return { previous } + }, + onError: (err, newProduct, context) => { + queryClient.setQueryData(['products'], context?.previous) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['products'] }) + }, + }) +} +``` + +### Pattern 4: Native Module Integration + +```typescript +// services/haptics.ts +import * as Haptics from 'expo-haptics' +import { Platform } from 'react-native' + +export const haptics = { + light: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) + } + }, + medium: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium) + } + }, + heavy: () => { + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy) + } + }, + success: () => { + if (Platform.OS !== 'web') { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) + } + }, + error: () => { + if (Platform.OS !== 'web') { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error) + } + }, +} + +// services/biometrics.ts +import * as LocalAuthentication from 'expo-local-authentication' + +export async function authenticateWithBiometrics(): Promise { + const hasHardware = await LocalAuthentication.hasHardwareAsync() + if (!hasHardware) return false + + const isEnrolled = await LocalAuthentication.isEnrolledAsync() + if (!isEnrolled) return false + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate to continue', + fallbackLabel: 'Use passcode', + disableDeviceFallback: false, + }) + + return result.success +} + +// services/notifications.ts +import * as Notifications from 'expo-notifications' +import { Platform } from 'react-native' +import Constants from 'expo-constants' + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + }), +}) + +export async function registerForPushNotifications() { + let token: string | undefined + + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + vibrationPattern: [0, 250, 250, 250], + }) + } + + const { status: existingStatus } = await Notifications.getPermissionsAsync() + let finalStatus = existingStatus + + if (existingStatus !== 'granted') { + const { status } = await Notifications.requestPermissionsAsync() + finalStatus = status + } + + if (finalStatus !== 'granted') { + return null + } + + const projectId = Constants.expoConfig?.extra?.eas?.projectId + token = (await Notifications.getExpoPushTokenAsync({ projectId })).data + + return token +} +``` + +### Pattern 5: Platform-Specific Code + +```typescript +// components/ui/Button.tsx +import { Platform, Pressable, StyleSheet, Text, ViewStyle } from 'react-native' +import * as Haptics from 'expo-haptics' +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated' + +const AnimatedPressable = Animated.createAnimatedComponent(Pressable) + +interface ButtonProps { + title: string + onPress: () => void + variant?: 'primary' | 'secondary' | 'outline' + disabled?: boolean +} + +export function Button({ + title, + onPress, + variant = 'primary', + disabled = false, +}: ButtonProps) { + const scale = useSharedValue(1) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ scale: scale.value }], + })) + + const handlePressIn = () => { + scale.value = withSpring(0.95) + if (Platform.OS !== 'web') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light) + } + } + + const handlePressOut = () => { + scale.value = withSpring(1) + } + + return ( + + {title} + + ) +} + +// Platform-specific files +// Button.ios.tsx - iOS-specific implementation +// Button.android.tsx - Android-specific implementation +// Button.web.tsx - Web-specific implementation + +// Or use Platform.select +const styles = StyleSheet.create({ + button: { + paddingVertical: 12, + paddingHorizontal: 24, + borderRadius: 8, + alignItems: 'center', + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + android: { + elevation: 4, + }, + }), + }, + primary: { + backgroundColor: '#007AFF', + }, + secondary: { + backgroundColor: '#5856D6', + }, + outline: { + backgroundColor: 'transparent', + borderWidth: 1, + borderColor: '#007AFF', + }, + disabled: { + opacity: 0.5, + }, + text: { + fontSize: 16, + fontWeight: '600', + }, + primaryText: { + color: '#FFFFFF', + }, + secondaryText: { + color: '#FFFFFF', + }, + outlineText: { + color: '#007AFF', + }, +}) +``` + +### Pattern 6: Performance Optimization + +```typescript +// components/ProductList.tsx +import { FlashList } from '@shopify/flash-list' +import { memo, useCallback } from 'react' + +interface ProductListProps { + products: Product[] + onProductPress: (id: string) => void +} + +// Memoize list item +const ProductItem = memo(function ProductItem({ + item, + onPress, +}: { + item: Product + onPress: (id: string) => void +}) { + const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]) + + return ( + + + {item.name} + ${item.price} + + ) +}) + +export function ProductList({ products, onProductPress }: ProductListProps) { + const renderItem = useCallback( + ({ item }: { item: Product }) => ( + + ), + [onProductPress] + ) + + const keyExtractor = useCallback((item: Product) => item.id, []) + + return ( + + ) +} +``` + +## EAS Build & Submit + +```json +// eas.json +{ + "cli": { "version": ">= 5.0.0" }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "ios": { "simulator": true } + }, + "preview": { + "distribution": "internal", + "android": { "buildType": "apk" } + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": { + "ios": { "appleId": "your@email.com", "ascAppId": "123456789" }, + "android": { "serviceAccountKeyPath": "./google-services.json" } + } + } +} +``` + +```bash +# Build commands +eas build --platform ios --profile development +eas build --platform android --profile preview +eas build --platform all --profile production + +# Submit to stores +eas submit --platform ios +eas submit --platform android + +# OTA updates +eas update --branch production --message "Bug fixes" +``` + +## Best Practices + +### Do's +- **Use Expo** - Faster development, OTA updates, managed native code +- **FlashList over FlatList** - Better performance for long lists +- **Memoize components** - Prevent unnecessary re-renders +- **Use Reanimated** - 60fps animations on native thread +- **Test on real devices** - Simulators miss real-world issues + +### Don'ts +- **Don't inline styles** - Use StyleSheet.create for performance +- **Don't fetch in render** - Use useEffect or React Query +- **Don't ignore platform differences** - Test on both iOS and Android +- **Don't store secrets in code** - Use environment variables +- **Don't skip error boundaries** - Mobile crashes are unforgiving + +## Resources + +- [Expo Documentation](https://docs.expo.dev/) +- [Expo Router](https://docs.expo.dev/router/introduction/) +- [React Native Performance](https://reactnative.dev/docs/performance) +- [FlashList](https://shopify.github.io/flash-list/) diff --git a/plugins/frontend-mobile-development/skills/react-state-management/SKILL.md b/plugins/frontend-mobile-development/skills/react-state-management/SKILL.md new file mode 100644 index 0000000..dd20005 --- /dev/null +++ b/plugins/frontend-mobile-development/skills/react-state-management/SKILL.md @@ -0,0 +1,429 @@ +--- +name: react-state-management +description: Master modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use when setting up global state, managing server state, or choosing between state management solutions. +--- + +# React State Management + +Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization. + +## When to Use This Skill + +- Setting up global state management in a React app +- Choosing between Redux Toolkit, Zustand, or Jotai +- Managing server state with React Query or SWR +- Implementing optimistic updates +- Debugging state-related issues +- Migrating from legacy Redux to modern patterns + +## Core Concepts + +### 1. State Categories + +| Type | Description | Solutions | +|------|-------------|-----------| +| **Local State** | Component-specific, UI state | useState, useReducer | +| **Global State** | Shared across components | Redux Toolkit, Zustand, Jotai | +| **Server State** | Remote data, caching | React Query, SWR, RTK Query | +| **URL State** | Route parameters, search | React Router, nuqs | +| **Form State** | Input values, validation | React Hook Form, Formik | + +### 2. Selection Criteria + +``` +Small app, simple state → Zustand or Jotai +Large app, complex state → Redux Toolkit +Heavy server interaction → React Query + light client state +Atomic/granular updates → Jotai +``` + +## Quick Start + +### Zustand (Simplest) + +```typescript +// store/useStore.ts +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +interface AppState { + user: User | null + theme: 'light' | 'dark' + setUser: (user: User | null) => void + toggleTheme: () => void +} + +export const useStore = create()( + devtools( + persist( + (set) => ({ + user: null, + theme: 'light', + setUser: (user) => set({ user }), + toggleTheme: () => set((state) => ({ + theme: state.theme === 'light' ? 'dark' : 'light' + })), + }), + { name: 'app-storage' } + ) + ) +) + +// Usage in component +function Header() { + const { user, theme, toggleTheme } = useStore() + return ( +
+ {user?.name} + +
+ ) +} +``` + +## Patterns + +### Pattern 1: Redux Toolkit with TypeScript + +```typescript +// store/index.ts +import { configureStore } from '@reduxjs/toolkit' +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import userReducer from './slices/userSlice' +import cartReducer from './slices/cartSlice' + +export const store = configureStore({ + reducer: { + user: userReducer, + cart: cartReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ['persist/PERSIST'], + }, + }), +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch + +// Typed hooks +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector +``` + +```typescript +// store/slices/userSlice.ts +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit' + +interface User { + id: string + email: string + name: string +} + +interface UserState { + current: User | null + status: 'idle' | 'loading' | 'succeeded' | 'failed' + error: string | null +} + +const initialState: UserState = { + current: null, + status: 'idle', + error: null, +} + +export const fetchUser = createAsyncThunk( + 'user/fetchUser', + async (userId: string, { rejectWithValue }) => { + try { + const response = await fetch(`/api/users/${userId}`) + if (!response.ok) throw new Error('Failed to fetch user') + return await response.json() + } catch (error) { + return rejectWithValue((error as Error).message) + } + } +) + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + setUser: (state, action: PayloadAction) => { + state.current = action.payload + state.status = 'succeeded' + }, + clearUser: (state) => { + state.current = null + state.status = 'idle' + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchUser.pending, (state) => { + state.status = 'loading' + state.error = null + }) + .addCase(fetchUser.fulfilled, (state, action) => { + state.status = 'succeeded' + state.current = action.payload + }) + .addCase(fetchUser.rejected, (state, action) => { + state.status = 'failed' + state.error = action.payload as string + }) + }, +}) + +export const { setUser, clearUser } = userSlice.actions +export default userSlice.reducer +``` + +### Pattern 2: Zustand with Slices (Scalable) + +```typescript +// store/slices/createUserSlice.ts +import { StateCreator } from 'zustand' + +export interface UserSlice { + user: User | null + isAuthenticated: boolean + login: (credentials: Credentials) => Promise + logout: () => void +} + +export const createUserSlice: StateCreator< + UserSlice & CartSlice, // Combined store type + [], + [], + UserSlice +> = (set, get) => ({ + user: null, + isAuthenticated: false, + login: async (credentials) => { + const user = await authApi.login(credentials) + set({ user, isAuthenticated: true }) + }, + logout: () => { + set({ user: null, isAuthenticated: false }) + // Can access other slices + // get().clearCart() + }, +}) + +// store/index.ts +import { create } from 'zustand' +import { createUserSlice, UserSlice } from './slices/createUserSlice' +import { createCartSlice, CartSlice } from './slices/createCartSlice' + +type StoreState = UserSlice & CartSlice + +export const useStore = create()((...args) => ({ + ...createUserSlice(...args), + ...createCartSlice(...args), +})) + +// Selective subscriptions (prevents unnecessary re-renders) +export const useUser = () => useStore((state) => state.user) +export const useCart = () => useStore((state) => state.cart) +``` + +### Pattern 3: Jotai for Atomic State + +```typescript +// atoms/userAtoms.ts +import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +// Basic atom +export const userAtom = atom(null) + +// Derived atom (computed) +export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null) + +// Atom with localStorage persistence +export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light') + +// Async atom +export const userProfileAtom = atom(async (get) => { + const user = get(userAtom) + if (!user) return null + const response = await fetch(`/api/users/${user.id}/profile`) + return response.json() +}) + +// Write-only atom (action) +export const logoutAtom = atom(null, (get, set) => { + set(userAtom, null) + set(cartAtom, []) + localStorage.removeItem('token') +}) + +// Usage +function Profile() { + const [user] = useAtom(userAtom) + const [, logout] = useAtom(logoutAtom) + const [profile] = useAtom(userProfileAtom) // Suspense-enabled + + return ( + }> + + + ) +} +``` + +### Pattern 4: React Query for Server State + +```typescript +// hooks/useUsers.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' + +// Query keys factory +export const userKeys = { + all: ['users'] as const, + lists: () => [...userKeys.all, 'list'] as const, + list: (filters: UserFilters) => [...userKeys.lists(), filters] as const, + details: () => [...userKeys.all, 'detail'] as const, + detail: (id: string) => [...userKeys.details(), id] as const, +} + +// Fetch hook +export function useUsers(filters: UserFilters) { + return useQuery({ + queryKey: userKeys.list(filters), + queryFn: () => fetchUsers(filters), + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime) + }) +} + +// Single user hook +export function useUser(id: string) { + return useQuery({ + queryKey: userKeys.detail(id), + queryFn: () => fetchUser(id), + enabled: !!id, // Don't fetch if no id + }) +} + +// Mutation with optimistic update +export function useUpdateUser() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: updateUser, + onMutate: async (newUser) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: userKeys.detail(newUser.id) }) + + // Snapshot previous value + const previousUser = queryClient.getQueryData(userKeys.detail(newUser.id)) + + // Optimistically update + queryClient.setQueryData(userKeys.detail(newUser.id), newUser) + + return { previousUser } + }, + onError: (err, newUser, context) => { + // Rollback on error + queryClient.setQueryData( + userKeys.detail(newUser.id), + context?.previousUser + ) + }, + onSettled: (data, error, variables) => { + // Refetch after mutation + queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) }) + }, + }) +} +``` + +### Pattern 5: Combining Client + Server State + +```typescript +// Zustand for client state +const useUIStore = create((set) => ({ + sidebarOpen: true, + modal: null, + toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })), + openModal: (modal) => set({ modal }), + closeModal: () => set({ modal: null }), +})) + +// React Query for server state +function Dashboard() { + const { sidebarOpen, toggleSidebar } = useUIStore() + const { data: users, isLoading } = useUsers({ active: true }) + const { data: stats } = useStats() + + if (isLoading) return + + return ( +
+ +
+ + +
+
+ ) +} +``` + +## Best Practices + +### Do's +- **Colocate state** - Keep state as close to where it's used as possible +- **Use selectors** - Prevent unnecessary re-renders with selective subscriptions +- **Normalize data** - Flatten nested structures for easier updates +- **Type everything** - Full TypeScript coverage prevents runtime errors +- **Separate concerns** - Server state (React Query) vs client state (Zustand) + +### Don'ts +- **Don't over-globalize** - Not everything needs to be in global state +- **Don't duplicate server state** - Let React Query manage it +- **Don't mutate directly** - Always use immutable updates +- **Don't store derived data** - Compute it instead +- **Don't mix paradigms** - Pick one primary solution per category + +## Migration Guides + +### From Legacy Redux to RTK + +```typescript +// Before (legacy Redux) +const ADD_TODO = 'ADD_TODO' +const addTodo = (text) => ({ type: ADD_TODO, payload: text }) +function todosReducer(state = [], action) { + switch (action.type) { + case ADD_TODO: + return [...state, { text: action.payload, completed: false }] + default: + return state + } +} + +// After (Redux Toolkit) +const todosSlice = createSlice({ + name: 'todos', + initialState: [], + reducers: { + addTodo: (state, action: PayloadAction) => { + // Immer allows "mutations" + state.push({ text: action.payload, completed: false }) + }, + }, +}) +``` + +## Resources + +- [Redux Toolkit Documentation](https://redux-toolkit.js.org/) +- [Zustand GitHub](https://github.com/pmndrs/zustand) +- [Jotai Documentation](https://jotai.org/) +- [TanStack Query](https://tanstack.com/query) diff --git a/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md b/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md new file mode 100644 index 0000000..dfcf135 --- /dev/null +++ b/plugins/frontend-mobile-development/skills/tailwind-design-system/SKILL.md @@ -0,0 +1,666 @@ +--- +name: tailwind-design-system +description: Build scalable design systems with Tailwind CSS, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns. +--- + +# Tailwind Design System + +Build production-ready design systems with Tailwind CSS, including design tokens, component variants, responsive patterns, and accessibility. + +## When to Use This Skill + +- Creating a component library with Tailwind +- Implementing design tokens and theming +- Building responsive and accessible components +- Standardizing UI patterns across a codebase +- Migrating to or extending Tailwind CSS +- Setting up dark mode and color schemes + +## Core Concepts + +### 1. Design Token Hierarchy + +``` +Brand Tokens (abstract) + └── Semantic Tokens (purpose) + └── Component Tokens (specific) + +Example: + blue-500 → primary → button-bg +``` + +### 2. Component Architecture + +``` +Base styles → Variants → Sizes → States → Overrides +``` + +## Quick Start + +```typescript +// tailwind.config.ts +import type { Config } from 'tailwindcss' + +const config: Config = { + content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], + darkMode: 'class', + theme: { + extend: { + colors: { + // Semantic color tokens + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + border: 'hsl(var(--border))', + ring: 'hsl(var(--ring))', + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +} + +export default config +``` + +```css +/* globals.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} +``` + +## Patterns + +### Pattern 1: CVA (Class Variance Authority) Components + +```typescript +// components/ui/button.tsx +import { cva, type VariantProps } from 'class-variance-authority' +import { forwardRef } from 'react' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + // Base styles + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +Button.displayName = 'Button' + +export { Button, buttonVariants } + +// Usage + + + +``` + +### Pattern 2: Compound Components + +```typescript +// components/ui/card.tsx +import { cn } from '@/lib/utils' +import { forwardRef } from 'react' + +const Card = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = forwardRef>( + ({ className, ...props }, ref) => ( +

+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } + +// Usage + + + Account + Manage your account settings + + +
...
+
+ + + +
+``` + +### Pattern 3: Form Components + +```typescript +// components/ui/input.tsx +import { forwardRef } from 'react' +import { cn } from '@/lib/utils' + +export interface InputProps extends React.InputHTMLAttributes { + error?: string +} + +const Input = forwardRef( + ({ className, type, error, ...props }, ref) => { + return ( +
+ + {error && ( + + )} +
+ ) + } +) +Input.displayName = 'Input' + +// components/ui/label.tsx +import { cva, type VariantProps } from 'class-variance-authority' + +const labelVariants = cva( + 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' +) + +const Label = forwardRef>( + ({ className, ...props }, ref) => ( +