From 165f5907064b5746a8d5ccdd41868f2de456835b Mon Sep 17 00:00:00 2001 From: Emmanuel Andre Date: Thu, 4 Dec 2025 19:29:03 +0800 Subject: [PATCH 1/4] feat: add comprehensive workshop improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Prompt Engineering vs Vibe Coding slides (5 slides) - Add Addressing Common Concerns slides (9 slides) - Create docs 12-18 (documentation, code review, security, performance, CI/CD, advanced topics) - Create my-api-project example with CLAUDE.md and migrations - Merge best-practice-feature-doc.md into doc 12 (feature documentation deep dive) - Update prompts.md with reference examples - Update WORKSHOP_GUIDE.md with reference example section - Fix navigation links in docs 08, 11 - Fix dead example links in docs 04 - Update README.md with new doc entries - Regenerate PDF slides 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 8 + WORKSHOP_GUIDE.md | 31 + claude-code-interactive-tutorial.pdf | Bin 71845 -> 88186 bytes create_interactive_presentation_v2.py | 260 ++++ docs/04-claude-md.md | 5 +- docs/08-feature-workflow.md | 2 +- docs/11-git-workflow.md | 2 +- docs/12-documentation-organization.md | 1195 +++++++++++++++++ docs/13-documentation-writing.md | 556 ++++++++ docs/14-code-review.md | 475 +++++++ docs/15-security.md | 540 ++++++++ docs/16-performance.md | 601 +++++++++ docs/17-ci-cd.md | 715 ++++++++++ docs/18-advanced-topics.md | 571 ++++++++ examples/my-api-project/CLAUDE.md | 277 ++++ examples/my-api-project/README.md | 74 + .../migrations/001_create_users.down.sql | 15 + .../migrations/001_create_users.up.sql | 35 + prompts.md | 89 +- 19 files changed, 5429 insertions(+), 22 deletions(-) create mode 100644 docs/12-documentation-organization.md create mode 100644 docs/13-documentation-writing.md create mode 100644 docs/14-code-review.md create mode 100644 docs/15-security.md create mode 100644 docs/16-performance.md create mode 100644 docs/17-ci-cd.md create mode 100644 docs/18-advanced-topics.md create mode 100644 examples/my-api-project/CLAUDE.md create mode 100644 examples/my-api-project/README.md create mode 100644 examples/my-api-project/migrations/001_create_users.down.sql create mode 100644 examples/my-api-project/migrations/001_create_users.up.sql diff --git a/README.md b/README.md index 34fa33b..b806f05 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ A comprehensive guide to working effectively with Claude Code (claude.ai/code) f ### Best Practices - [Git Workflow](./docs/11-git-workflow.md) - Branch naming, commits, and PR management +- [Documentation Organization](./docs/12-documentation-organization.md) - Structure for plans, architecture, specs, and rules +- [Documentation Writing](./docs/13-documentation-writing.md) - API docs, code comments, and ADRs +- [Code Review](./docs/14-code-review.md) - Reviewing AI-generated code effectively +- [Security Practices](./docs/15-security.md) - Authentication, validation, and secrets management +- [Performance Optimization](./docs/16-performance.md) - Database, API, and frontend performance +- [CI/CD and Deployment](./docs/17-ci-cd.md) - GitHub Actions and deployment pipelines +- [Advanced Topics](./docs/18-advanced-topics.md) - MCP servers, multi-repo, and legacy code ### Reference - [Troubleshooting](./docs/19-troubleshooting.md) - Common issues and solutions @@ -30,6 +37,7 @@ A comprehensive guide to working effectively with Claude Code (claude.ai/code) f ### Examples - [CLAUDE.md Template](./examples/claude-md-template.md) - Production-ready project configuration +- [my-api-project](./examples/my-api-project/) - Workshop reference example (Go API with PostgreSQL) ### Workshop Materials - [Interactive Tutorial Slides](./claude-code-interactive-tutorial.pdf) - 40+ slide hands-on workshop diff --git a/WORKSHOP_GUIDE.md b/WORKSHOP_GUIDE.md index fc836ab..e2fbd92 100644 --- a/WORKSHOP_GUIDE.md +++ b/WORKSHOP_GUIDE.md @@ -124,9 +124,40 @@ python3 create_interactive_presentation_v2.py Both commands generate PDFs. The v2 script also generates `prompts.md`. +## Using the Reference Example + +The `examples/my-api-project/` directory contains a complete reference implementation. + +### What's Included +- `CLAUDE.md` - Production-ready project configuration +- `migrations/001_create_users.up.sql` - Database migration with triggers +- `migrations/001_create_users.down.sql` - Rollback migration + +### How to Use During Workshop + +1. **Attendees complete exercises independently** first +2. **After each exercise**, show the reference example for comparison +3. **Discuss differences** - different approaches are OK if they work! +4. **Do NOT show before exercise** - prevents learning + +### When to Show Reference + +| After Exercise | Show | +|----------------|------| +| Page 9 (Create CLAUDE.md) | `examples/my-api-project/CLAUDE.md` | +| Page 15 (Review Schema) | `examples/my-api-project/migrations/` | + +### Discussion Points + +- Reference CLAUDE.md includes automatic `updated_at` trigger +- Reference migration has both up and down versions +- Compare attendee outputs - different is OK if it works! +- Highlight patterns: indexes, constraints, comments + ## Post-Workshop Share with attendees: - Full documentation in `/docs` folder - CLAUDE.md template in `/examples` +- Reference project in `/examples/my-api-project` - Encourage them to try the final exercise at home diff --git a/claude-code-interactive-tutorial.pdf b/claude-code-interactive-tutorial.pdf index 40aab14ee0063eafe0737a0fb96a3459ee568281..ae187d16ba010a75cae955a2bf01970906ca24da 100644 GIT binary patch literal 88186 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 diff --git a/create_interactive_presentation_v2.py b/create_interactive_presentation_v2.py index 79c09a1..502d5b9 100644 --- a/create_interactive_presentation_v2.py +++ b/create_interactive_presentation_v2.py @@ -209,8 +209,69 @@ def export_prompts_to_markdown(): f.write('\n```\n\n') f.write(f'**EXPECTED RESULT:**\n') f.write(f'{item["expected"]}\n\n') + + # Add reference examples for specific pages + if item["page"] == 9: + f.write('\n**REFERENCE EXAMPLE:**\n') + f.write('Compare your generated CLAUDE.md with `examples/my-api-project/CLAUDE.md` to see a production-ready example.\n\n') + elif item["page"] == 15: + f.write('\n**REFERENCE EXAMPLE:**\n') + f.write('See `examples/my-api-project/migrations/001_create_users.up.sql` for a complete migration including:\n') + f.write('- Proper index on email column\n') + f.write('- Automatic updated_at trigger\n') + f.write('- Both up.sql and down.sql files\n\n') + f.write('---\n\n') + # Add new documentation organization exercises at the end + f.write('## NEW: Documentation Organization Exercise\n\n') + f.write('**PROMPT:**\n```\n') + f.write('''I'm starting a new project. Help me decide on documentation structure. + +Project details: +- Solo developer +- 3-month project +- Single Go API service +- ~40 tasks estimated + +Based on these factors, should I use: +A) Simple flat structure (plan.md, architecture.md at root) +B) Nested structure (project/planning/, project/specs/) + +Create the appropriate documentation files for my choice. +Include: +- plan.md or devplan.md with milestones +- architecture.md with system design +- requirements.md with key features +''') + f.write('```\n\n') + f.write('**EXPECTED RESULT:**\n') + f.write('Claude recommends nested structure (40 tasks, 3 months) and creates starter files\n\n') + f.write('**REFERENCE:**\n') + f.write('See [docs/12-documentation-organization.md](docs/12-documentation-organization.md) for decision criteria and templates.\n\n') + f.write('---\n\n') + + f.write('## NEW: Migrate Flat to Nested Structure\n\n') + f.write('**PROMPT:**\n```\n') + f.write('''My project has grown. I started with simple flat docs: +- plan.md +- architecture.md +- requirements.md + +Now I have 50+ tasks across 5 features. Help me migrate to +the nested structure: +- Move plan.md content to project/planning/devplan.md +- Create project/planning/devprogress.md for tracking +- Split requirements into project/specs/ by feature +- Keep architecture.md updated + +Preserve all existing content during migration. +''') + f.write('```\n\n') + f.write('**EXPECTED RESULT:**\n') + f.write('Documentation migrated to nested structure with content preserved\n\n') + f.write('---\n\n') + def create_presentation(): """Generate the complete interactive presentation""" doc = SimpleDocTemplate( @@ -233,6 +294,8 @@ def create_presentation(): # AGENDA create_content_slide(story, styles, "Workshop Agenda", [ "Part 1: Philosophy & Foundation", + "Addressing Common Concerns about AI-First Development", + "Prompt Engineering vs Vibe Coding", "Part 2: Getting Started Hands-On", "Part 3: Core Workflow (Interactive)", "Part 4: Testing Strategy (Live Demo)", @@ -293,6 +356,203 @@ def create_presentation(): ]) page_num += 1 + # ================= + # ADDRESSING COMMON CONCERNS + # ================= + create_section_slide(story, styles, "Addressing Common Concerns") + page_num += 1 + + create_content_slide(story, styles, "Common Concerns About AI-First Development", [ + "1. Code Quality & Reliability - Hallucinations, hidden bugs", + "2. Security Risks - Insecure patterns, data leakage", + "3. Maintainability & Technical Debt - Opaque code, inconsistent style", + "4. Design Integrity - Architecture drift, loss of intent", + "5. Testing & Validation - False sense of coverage", + "6. IP & Compliance - License ambiguity, auditability", + "7. Developer Experience - Skill atrophy, workflow disruption", + "8. Accountability - Who owns AI-generated bugs?" + ]) + page_num += 1 + + create_content_slide(story, styles, "Concern #1: Code Quality & Reliability", [ + ("The Concern:", [ + "AI generates syntactically correct but logically flawed code", + "Hidden bugs pass tests but fail in edge cases", + "Engineers might trust AI output without validation" + ]), + ("How We Address It:", [ + "Human validates 100% - Every line is reviewed", + "Mandatory E2E tests catch integration issues", + "Pre-commit checks enforce quality gates", + "Three-layer review: Self → Automated → Peer" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Concern #2: Security Risks", [ + ("The Concern:", [ + "AI might introduce vulnerabilities (SQL injection, weak crypto)", + "Proprietary code exposed to cloud-based AI tools", + "Unknown dependencies with security issues" + ]), + ("How We Address It:", [ + "Security checklist in code review (see docs/15-security.md)", + "Automated security scanning in CI (gosec, npm audit)", + "Claude Code runs locally - your code stays on your machine", + "Explicit prompts for secure patterns" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Concern #3: Maintainability & Technical Debt", [ + ("The Concern:", [ + "AI-generated code hard to understand or maintain", + "Suggestions may not follow team conventions", + "Quick fixes pile up without proper review" + ]), + ("How We Address It:", [ + "CLAUDE.md defines all conventions - AI follows them", + "Consistent patterns across entire codebase", + "Human review catches non-idiomatic code", + "Refactor requests explicit in prompts" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Concern #4: Design Integrity", [ + ("The Concern:", [ + "AI optimizes for local solutions, not holistic design", + "Code may work but ignore design principles (SOLID)", + "Risk of skipping critical design thinking" + ]), + ("How We Address It:", [ + "Architecture documented in CLAUDE.md", + "Human writes specifications BEFORE AI implements", + "Design decisions in ADRs (Architecture Decision Records)", + "AI follows existing patterns, doesn't invent new ones" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Concern #5: Testing & Validation", [ + ("The Concern:", [ + "AI generates tests that don't cover real scenarios", + "Non-deterministic outputs hard to debug", + "AI-generated modules may not integrate well" + ]), + ("How We Address It:", [ + "Test-first approach - define tests before implementation", + "E2E tests validate complete user journeys", + "Human verifies test quality, not just coverage", + "Integration testing mandatory in CI" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Concerns #6-8: IP, Experience & Accountability", [ + ("IP & Compliance:", [ + "Review AI suggestions for license issues", + "Keep audit trail of AI-generated code in commits" + ]), + ("Developer Experience:", [ + "AI executes, Human validates - skills remain sharp", + "Review process maintains deep code understanding", + "Pair programming with AI, not replacement" + ]), + ("Accountability:", [ + "Human approves every PR - human owns the code", + "Clear handoffs document who validated what", + "Git history shows human review at each step" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "The Bottom Line", [ + "AI-First ≠ AI-Only", + ("The methodology addresses concerns through:", [ + "Systematic validation at every step", + "Comprehensive testing (E2E + Unit)", + "Clear human ownership and accountability", + "Documented conventions in CLAUDE.md", + "Quality gates that must pass before merge" + ]), + "Result: Faster development WITH maintained quality" + ]) + page_num += 1 + + # ================= + # PROMPT ENGINEERING VS VIBE CODING + # ================= + create_section_slide(story, styles, "Prompt Engineering vs Vibe Coding") + page_num += 1 + + create_content_slide(story, styles, "What is Vibe Coding?", [ + "Definition: Informal, exploratory approach with minimal instructions", + ("Characteristics:", [ + "Speed and experimentation over precision", + "Relies on AI to 'guess' or 'fill in' intent", + "Minimal context provided" + ]), + "Example: 'Make something that sorts numbers'", + ("Result:", [ + "May work quickly for prototypes", + "Unpredictable or suboptimal code", + "Hard to maintain and debug" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "What is Prompt Engineering?", [ + "Definition: Precise, structured inputs to guide AI behavior", + ("Characteristics:", [ + "Context, constraints, and examples provided", + "Understanding how the model interprets language", + "Clear success criteria defined" + ]), + "Example: 'Write a Python function to sort integers ascending, no built-in sort'", + ("Result:", [ + "Predictable, production-quality code", + "Easier to maintain and extend", + "Consistent with project standards" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Side-by-Side Comparison", [ + ("Speed:", [ + "Vibe: Fast initial results", + "Prompt: Efficient overall (less rework)" + ]), + ("Quality:", [ + "Vibe: Unpredictable, may need fixes", + "Prompt: Consistent, meets requirements" + ]), + ("Maintenance:", [ + "Vibe: Harder - unclear intent", + "Prompt: Easier - documented approach" + ]), + ("Best For:", [ + "Vibe: Quick prototypes, exploration", + "Prompt: Production code, team projects" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "This Workshop Uses Prompt Engineering", [ + "We teach structured, production-quality prompting", + ("The AI-First workflow:", [ + "Human writes detailed specification", + "AI executes within defined boundaries", + "Human validates every output" + ]), + ("Flow: Spec → Schema → Code → Tests", [ + "Each step has clear inputs and outputs", + "Nothing left to 'vibes' or guesswork" + ]), + "Result: Code you can confidently ship to production" + ]) + page_num += 1 + # ================= # PART 2: GETTING STARTED # ================= diff --git a/docs/04-claude-md.md b/docs/04-claude-md.md index f2a49b5..293a48a 100644 --- a/docs/04-claude-md.md +++ b/docs/04-claude-md.md @@ -259,9 +259,8 @@ git commit -m "docs: update CLAUDE.md with new API patterns" ## Example Projects -- [API Service](../examples/api-service-claude.md) -- [React App](../examples/react-app-claude.md) -- [Microservices](../examples/microservices-claude.md) +- [CLAUDE.md Template](../examples/claude-md-template.md) - Comprehensive production-ready template +- [my-api-project](../examples/my-api-project/CLAUDE.md) - Workshop reference example (Go API) --- diff --git a/docs/08-feature-workflow.md b/docs/08-feature-workflow.md index f64f99a..9dc6e53 100644 --- a/docs/08-feature-workflow.md +++ b/docs/08-feature-workflow.md @@ -698,4 +698,4 @@ gh pr create --title "feat: Add password reset functionality" \ --- -**Prev:** [AI-First Workflow](./07-ai-first-workflow.md) | **Next:** [Real-World Examples](./09-real-world-examples.md) +**Prev:** [AI-First Workflow](./07-ai-first-workflow.md) | **Next:** [Project Planning & Documentation Structure](./09-project-planning-structure.md) diff --git a/docs/11-git-workflow.md b/docs/11-git-workflow.md index b0f7c8b..428aee2 100644 --- a/docs/11-git-workflow.md +++ b/docs/11-git-workflow.md @@ -331,4 +331,4 @@ git config --global alias.lg "log --graph --oneline --all" --- -**Prev:** [Scaling to Large Projects](./10-scaling-large-projects.md) | **Next:** [Documentation Strategies](./12-documentation.md) +**Prev:** [Scaling to Large Projects](./10-scaling-large-projects.md) | **Next:** [Documentation Organization](./12-documentation-organization.md) diff --git a/docs/12-documentation-organization.md b/docs/12-documentation-organization.md new file mode 100644 index 0000000..9c0dff9 --- /dev/null +++ b/docs/12-documentation-organization.md @@ -0,0 +1,1195 @@ +# Documentation Organization + +How to structure your project documentation for effective AI-first development. + +## Why Documentation Structure Matters + +In AI-first development, your documentation serves multiple purposes: +- **Context for Claude** - Persistent knowledge across sessions +- **Team alignment** - Single source of truth for conventions +- **Progress tracking** - Clear visibility into project state +- **Knowledge preservation** - Decisions and rationale captured + +The right structure depends on your project size, team, and duration. + +## The Two Patterns + +### Pattern 1: Simple Flat Structure + +Best for: Solo projects, < 20 tasks, short duration (weeks) + +``` +project-root/ +├── CLAUDE.md # Project configuration for Claude +├── README.md # Project overview +├── plan.md # Development roadmap +├── architecture.md # System design +├── requirements.md # Feature requirements +├── ui-specs.md # UI specifications (if applicable) +└── src/ # Source code +``` + +**Advantages:** +- Quick to set up +- Easy to navigate +- Low overhead +- All context in one place + +**When to use:** +- Solo developer or pair +- Project duration < 1 month +- Single service/application +- < 20 total tasks + +### Pattern 2: Nested Directory Structure + +Best for: Teams, 20+ tasks, long-term projects + +``` +project-root/ +├── CLAUDE.md # Project configuration +├── README.md # Project overview +├── project/ # Documentation hub +│ ├── planning/ # Master plans and progress +│ │ ├── devplan.md +│ │ ├── devprogress.md +│ │ ├── database.md +│ │ └── sitemap.md +│ ├── specs/ # Feature specifications +│ │ ├── 01-auth.md +│ │ ├── 02-dashboard.md +│ │ └── ... +│ ├── architecture/ # Architecture documents +│ │ ├── backend.md +│ │ ├── frontend.md +│ │ └── infrastructure.md +│ ├── sessions/ # Session summaries +│ └── archive/ # Historical docs +└── src/ +``` + +**Advantages:** +- Scales with project complexity +- Clear separation of concerns +- Better for team collaboration +- Supports phased development + +**When to use:** +- Team of 2+ developers +- Project duration > 1 month +- Multiple services/components +- 20+ tasks across phases + +> See [Project Planning & Documentation Structure](./09-project-planning-structure.md) for detailed guidance on the nested pattern. + +## Decision Matrix + +| Factor | Flat Structure | Nested Structure | +|--------|---------------|------------------| +| Team size | 1-2 developers | 3+ developers | +| Project duration | < 1 month | > 1 month | +| Task count | < 20 tasks | 20+ tasks | +| Services | Single service | Multiple services | +| Phases | 1-2 phases | 3+ phases | + +**Decision flowchart:** + +``` +Is your project > 1 month duration? +├── No → Use Flat Structure +└── Yes → Do you have 20+ tasks? + ├── No → Use Flat Structure + └── Yes → Use Nested Structure +``` + +--- + +## Core Documentation Files + +These files are essential regardless of which pattern you choose. + +### plan.md / devplan.md + +**Purpose:** Development roadmap, task tracking, and progress visibility. + +**Template (Flat Pattern):** + +```markdown +# Development Plan + +## Overview +[One-paragraph project description] + +## Goals +- [ ] Goal 1: [Description] +- [ ] Goal 2: [Description] +- [ ] Goal 3: [Description] + +## Milestones + +### Milestone 1: [Name] +**Target:** [Date or sprint] +**Status:** In Progress / Complete / Blocked + +Tasks: +- [ ] Task 1.1: [Description] +- [ ] Task 1.2: [Description] +- [x] Task 1.3: [Completed task] + +### Milestone 2: [Name] +**Target:** [Date or sprint] +**Status:** Not Started + +Tasks: +- [ ] Task 2.1: [Description] +- [ ] Task 2.2: [Description] + +## Current Focus + +**Active:** [Current task being worked on] +**Next:** [Next task after current] +**Blocked:** [Any blockers, or "None"] + +## Completed + +- [x] [Completed milestone or task] +- [x] [Completed milestone or task] + +## Notes + +[Any important context, decisions, or changes to the plan] +``` + +**Template (Nested Pattern - devplan.md):** + +```markdown +# Project Development Plan + +## Development Strategy + +### Core Principles +1. Database First - Schema before implementation +2. API First - Backend before frontend +3. Test Driven - Tests at each layer +4. Incremental - Follow dependency order +5. Vertical Slices - Complete stack per feature + +### Implementation Workflow (Per Feature) +1. DATABASE: Migration → Repository → Mock data +2. BACKEND API: Handlers → Validation → Permissions +3. API E2E TESTS: Cypress/Playwright tests → All pass +4. FRONTEND UI: Components → Forms → Integration +5. UI E2E TESTS: UI tests → All pass +6. DOCUMENTATION: API docs → Comments → Changelog + +### Dependency Map +``` +[Entity] → [Entity] → [Entity] +Example: Organizations → Users → Permissions → Features +``` + +### Development Phases + +#### Phase 0: Foundation +- [ ] Database setup +- [ ] Authentication +- [ ] Core infrastructure + +#### Phase 1: Core Features +- [ ] Feature A +- [ ] Feature B + +#### Phase 2: Advanced Features +- [ ] Feature C +- [ ] Feature D + +[Continue for all phases...] +``` + +--- + +### architecture.md + +**Purpose:** System design, component relationships, and technical decisions. + +**Template:** + +```markdown +# Architecture + +## System Overview + +[High-level description of what the system does and its main components] + +## Architecture Diagram + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Client │────▶│ API │────▶│ Database │ +│ (React) │ │ (Go) │ │ (PostgreSQL)│ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ Cache │ + │ (Redis) │ + └─────────────┘ +``` + +## Components + +### Component: [Name] +**Purpose:** [What it does] +**Technology:** [Tech stack] +**Key Files:** +- `path/to/main/file` +- `path/to/other/file` + +**Responsibilities:** +- Responsibility 1 +- Responsibility 2 + +### Component: [Name] +[Repeat for each component...] + +## Data Flow + +### Flow: [User Action] +1. User initiates [action] +2. Frontend sends request to [endpoint] +3. API validates and processes +4. Database updated +5. Response returned + +## Key Architectural Decisions + +| Decision | Options Considered | Choice | Rationale | +|----------|-------------------|--------|-----------| +| Database | PostgreSQL, MySQL, MongoDB | PostgreSQL | ACID compliance, JSON support | +| Auth | Sessions, JWT | JWT | Stateless, mobile-friendly | +| Cache | Redis, Memcached | Redis | Data structures, persistence | + +## External Dependencies + +| Dependency | Purpose | Documentation | +|------------|---------|---------------| +| Stripe | Payments | [Link] | +| SendGrid | Email | [Link] | + +## Security Considerations + +- All API endpoints require authentication except [exceptions] +- Sensitive data encrypted at rest +- HTTPS enforced in production +``` + +--- + +### requirements.md + +**Purpose:** Functional and non-functional requirements with acceptance criteria. + +**Template:** + +```markdown +# Requirements + +## Functional Requirements + +### FR-001: [Feature Name] +**Priority:** High / Medium / Low +**Status:** Planned / In Progress / Complete + +**Description:** +[What the feature does from user perspective] + +**User Story:** +As a [role], I want to [action] so that [benefit]. + +**Acceptance Criteria:** +- [ ] Criterion 1: [Specific, testable condition] +- [ ] Criterion 2: [Specific, testable condition] +- [ ] Criterion 3: [Specific, testable condition] + +**Dependencies:** +- FR-XXX: [Dependent feature] + +--- + +### FR-002: [Feature Name] +[Repeat structure...] + +--- + +## Non-Functional Requirements + +### NFR-001: Performance +- Page load time < 3 seconds on 3G +- API response time < 500ms (p95) +- Support 1000 concurrent users + +### NFR-002: Security +- OWASP Top 10 compliance +- Data encrypted at rest and in transit +- Session timeout after 30 minutes inactivity + +### NFR-003: Availability +- 99.9% uptime SLA +- Automated failover +- Daily backups with 30-day retention + +### NFR-004: Accessibility +- WCAG 2.1 AA compliance +- Keyboard navigation support +- Screen reader compatible + +## Constraints + +- Must integrate with existing [system] +- Budget: [amount] +- Timeline: [deadline] +- Tech stack: [required technologies] +``` + +--- + +### ui-specs.md + +**Purpose:** Frontend specifications, design system, and page layouts. + +**Template:** + +```markdown +# UI Specifications + +## Design System + +### Colors +| Name | Hex | Usage | +|------|-----|-------| +| Primary | #3B82F6 | Buttons, links, accents | +| Secondary | #6B7280 | Secondary text, borders | +| Success | #10B981 | Success states | +| Error | #EF4444 | Error states | +| Background | #F9FAFB | Page background | + +### Typography +| Element | Font | Size | Weight | +|---------|------|------|--------| +| H1 | Inter | 36px | 700 | +| H2 | Inter | 24px | 600 | +| Body | Inter | 16px | 400 | +| Small | Inter | 14px | 400 | + +### Spacing +- Base unit: 4px +- Scale: 4, 8, 12, 16, 24, 32, 48, 64 + +### Components +- Buttons: Primary, Secondary, Ghost, Danger +- Inputs: Text, Select, Checkbox, Radio +- Cards: Default, Elevated, Interactive + +--- + +## Pages + +### Page: [Page Name] +**Route:** `/path` +**Access:** Public / Authenticated / Admin + +**Purpose:** +[What users accomplish on this page] + +**Layout:** +``` +┌─────────────────────────────────┐ +│ Header │ +├─────────┬───────────────────────┤ +│ Sidebar │ Main Content │ +│ │ ┌─────────────────┐ │ +│ │ │ Component A │ │ +│ │ └─────────────────┘ │ +│ │ ┌─────────────────┐ │ +│ │ │ Component B │ │ +│ │ └─────────────────┘ │ +└─────────┴───────────────────────┘ +``` + +**Components:** +| Component | Purpose | Data Source | +|-----------|---------|-------------| +| Header | Navigation | Auth state | +| Sidebar | Menu items | User role | +| Component A | [Purpose] | API: /endpoint | + +**States:** +- **Loading:** Skeleton placeholder +- **Empty:** "No items found" message with CTA +- **Error:** Error message with retry button +- **Success:** Data displayed in table/list + +**User Actions:** +| Action | Trigger | Result | +|--------|---------|--------| +| Create item | Click "Add" button | Modal opens | +| Delete item | Click trash icon | Confirmation dialog | +| Filter | Select dropdown | List filters | + +--- + +### Page: [Next Page] +[Repeat structure...] + +--- + +## User Flows + +### Flow: [Flow Name] +1. User lands on [page] +2. User clicks [element] +3. System displays [response] +4. User completes [action] +5. System confirms [result] + +## Responsive Breakpoints + +| Breakpoint | Width | Layout Changes | +|------------|-------|----------------| +| Mobile | < 640px | Single column, hamburger menu | +| Tablet | 640-1024px | Two columns, collapsible sidebar | +| Desktop | > 1024px | Full layout with fixed sidebar | +``` + +--- + +### README.md + +**Purpose:** Project entry point for developers and stakeholders. + +**Template:** + +```markdown +# Project Name + +[One-line description of what this project does] + +## Overview + +[2-3 paragraph description covering:] +- What problem it solves +- Who it's for +- Key features + +## Quick Start + +### Prerequisites +- Node.js 18+ +- PostgreSQL 16 +- Docker (optional) + +### Installation + +```bash +# Clone the repository +git clone [repo-url] +cd [project-name] + +# Install dependencies +npm install + +# Set up environment +cp .env.example .env +# Edit .env with your values + +# Run database migrations +npm run db:migrate + +# Start development server +npm run dev +``` + +### Verify Installation +```bash +# Run tests +npm test + +# Open in browser +open http://localhost:3000 +``` + +## Project Structure + +``` +├── src/ +│ ├── api/ # API routes and handlers +│ ├── components/ # React components +│ ├── lib/ # Shared utilities +│ └── pages/ # Page components +├── tests/ # Test files +├── docs/ # Documentation +└── scripts/ # Build and utility scripts +``` + +## Documentation + +- [Architecture](./architecture.md) +- [API Documentation](./docs/api.md) +- [Contributing Guide](./CONTRIBUTING.md) + +## Development + +### Commands + +| Command | Description | +|---------|-------------| +| `npm run dev` | Start development server | +| `npm run build` | Build for production | +| `npm test` | Run tests | +| `npm run lint` | Run linter | + +### Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `DATABASE_URL` | PostgreSQL connection string | Yes | +| `JWT_SECRET` | Secret for JWT signing | Yes | +| `PORT` | Server port (default: 3000) | No | + +## Contributing + +1. Create a feature branch: `git checkout -b feature/your-feature` +2. Make your changes +3. Run tests: `npm test` +4. Submit a pull request + +## License + +[License type] - see [LICENSE](./LICENSE) for details +``` + +--- + +### Rules & Memory + +For persistent context across Claude sessions, use these patterns: + +#### .claude/rules/ Directory + +Create project-specific rules that Claude follows: + +``` +.claude/ +└── rules/ + ├── coding-standards.md + ├── testing-requirements.md + └── git-conventions.md +``` + +**Example: coding-standards.md** +```markdown +# Coding Standards + +## Naming Conventions +- Functions: camelCase +- Components: PascalCase +- Constants: UPPER_SNAKE_CASE +- Files: kebab-case + +## Code Organization +- One component per file +- Group related files in feature folders +- Keep files under 300 lines + +## Required Patterns +- All API calls through service layer +- Error boundaries around async operations +- Loading states for all data fetching +``` + +#### Memory/Context Files + +For session continuity, maintain context files: + +**CONTEXT.md** (Session handoff) +```markdown +# Current Context + +## Last Session +**Date:** 2025-01-15 +**Focus:** Implementing user authentication +**Branch:** feature/user-auth + +## Current State +- [x] Database migration complete +- [x] Repository layer done +- [ ] API handlers in progress (2/4 done) +- [ ] Tests not started + +## Next Steps +1. Complete login handler +2. Add JWT middleware +3. Write E2E tests + +## Open Questions +- Should we implement refresh tokens? (Decided: Yes) +- Token expiry time? (Pending decision) + +## References +- Spec: project/specs/01-auth.md +- Migration: migrations/001_users.up.sql +``` + +--- + +## The Hybrid Approach + +Start simple, evolve as needed. + +### Starting Flat + +Begin with flat structure for new projects: + +``` +my-project/ +├── CLAUDE.md +├── README.md +├── plan.md +├── architecture.md +└── src/ +``` + +### Graduating to Nested + +When your project grows (20+ tasks, multiple phases), migrate: + +**Step 1:** Create the directory structure +```bash +mkdir -p project/{planning,specs,architecture,sessions} +``` + +**Step 2:** Move and split existing files +```bash +# Move plan.md content +mv plan.md project/planning/devplan.md +# Create progress tracker +touch project/planning/devprogress.md + +# Split architecture if needed +mv architecture.md project/architecture/overview.md + +# Create first spec from requirements +mv requirements.md project/specs/00-overview.md +``` + +**Step 3:** Update CLAUDE.md references +```markdown +## Documentation Structure + +This project uses the nested documentation pattern: +- Planning: `project/planning/` +- Specifications: `project/specs/` +- Architecture: `project/architecture/` +- Session notes: `project/sessions/` +``` + +### Migration Prompt + +Ask Claude to help migrate: + +``` +My project has grown and I need to migrate from flat to nested +documentation structure. Current files: +- plan.md (tasks and progress) +- architecture.md (system design) +- requirements.md (features) + +Please: +1. Create project/ directory structure +2. Split plan.md into devplan.md and devprogress.md +3. Move architecture.md to project/architecture/ +4. Split requirements into separate spec files per feature +5. Update CLAUDE.md to reference new structure + +Preserve all existing content during migration. +``` + +--- + +## Feature Documentation Deep Dive + +For larger features that span multiple days or involve complex requirements, use this comprehensive documentation approach. + +### The Five Document Types + +When planning a significant feature, create these five interconnected documents: + +| Document | Purpose | Location | +|----------|---------|----------| +| Planning | High-level overview, decisions, timeline | `/docs/planning/features/{feature}.md` | +| Architecture | Technical design, system interactions | `/docs/architecture/{feature}-architecture.md` | +| API Contracts | Endpoint specifications, request/response | `/docs/specs/{feature}/api-contracts.md` | +| User Journey | Personas, flows, edge cases | `/docs/specs/{feature}/user-journey.md` | +| UI Wireframes | Page layouts, component specs | `/docs/specs/{feature}/ui-wireframes.md` | + +### Feature Planning Document + +**Purpose:** High-level overview, key decisions, timeline, and success criteria. + +```markdown +# Feature: {Feature Name} + +**Status:** Planning | In Progress | Complete +**Priority:** High | Medium | Low +**Last Updated:** YYYY-MM-DD + +## Related Documentation +- [Architecture](../architecture/{feature}-architecture.md) +- [API Contracts](../specs/{feature}/api-contracts.md) +- [UI Wireframes](../specs/{feature}/ui-wireframes.md) +- [User Journey](../specs/{feature}/user-journey.md) + +## Overview +{2-3 paragraphs describing the feature, its value, and high-level approach} + +## Key Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| {Decision 1} | {Choice made} | {Why this choice} | + +## Scope + +### In Scope +- {Item 1} +- {Item 2} + +### Out of Scope (Deferred) +- {Item 1} - {Reason} + +## Implementation Phases + +### Phase 1: {Name} +- [ ] Task 1 +- [ ] Task 2 + +### Phase 2: {Name} +- [ ] Task 1 + +## Critical Files + +**Backend:** +- `path/to/file.go` - {Purpose} + +**Frontend:** +- `path/to/component.tsx` - {Purpose} + +**Database:** +- `migrations/xxx_create_table.sql` + +## Success Criteria + +### By End of Phase 1 +- [ ] Metric 1 achieved +- [ ] Metric 2 achieved +``` + +### Feature Architecture Document + +**Purpose:** Technical design, database schema, API design, and integration points. + +**Key Patterns:** + +**ASCII System Diagrams:** +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ UI │────▶│ API │────▶│ Queue │ +└─────────┘ └─────────┘ └────┬────┘ + │ + ┌────────────────┘ + ▼ + ┌──────────┐ ┌──────────┐ + │ Worker │────▶│ Database │ + └──────────┘ └──────────┘ +``` + +**Database Schema with Indexes:** +```sql +CREATE TABLE feature_items ( + id SERIAL PRIMARY KEY, + org_id INT NOT NULL REFERENCES organizations(id), + user_id INT NOT NULL REFERENCES users(id), + status VARCHAR(50) NOT NULL DEFAULT 'pending', + metadata JSONB, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_feature_items_org ON feature_items(org_id); +CREATE INDEX idx_feature_items_status ON feature_items(status); +``` + +**Pipeline Stage Tables:** + +| Stage | Duration | Description | +|-------|----------|-------------| +| 1. Validate | <1s | Input validation | +| 2. Process | ~5s | Core transformation | +| 3. Verify | ~10s | Output verification | + +### API Contracts Document + +**Purpose:** Complete endpoint specifications with request/response examples. + +```markdown +### POST /api/v1/items + +Create a new item. + +**Request:** +```json +{ + "name": "Example", + "type": "standard", + "options": { + "auto_process": true + } +} +``` + +**Response (201 Created):** +```json +{ + "id": 123, + "name": "Example", + "status": "pending", + "created_at": "2025-01-15T10:30:00Z" +} +``` + +**Error Responses:** +- `400` - Invalid request body +- `401` - Unauthorized +- `409` - Duplicate item +- `422` - Validation failed +``` + +**Error Code Table:** + +| Code | HTTP | Description | +|------|------|-------------| +| VALIDATION_FAILED | 422 | Input validation error | +| DUPLICATE_ITEM | 409 | Item already exists | + +### User Journey Document + +**Purpose:** User personas, step-by-step flows, edge cases. + +**Persona Template:** +```markdown +### Persona: Power User + +**Background:** Experienced with similar tools, uses features daily +**Goals:** +- Complete tasks quickly with minimal clicks +- Access advanced options when needed +**Pain Points:** +- Frustrated by unnecessary confirmations +- Needs keyboard shortcuts +``` + +**Journey Map:** +```markdown +### Journey: Create New Item + +**Duration:** 30-60 seconds +**Steps:** + +1. **Navigate** → User clicks "New Item" button + - System displays creation form + +2. **Configure** → User fills required fields + - System validates in real-time + +3. **Submit** → User clicks "Create" + - System shows progress indicator + +4. **Complete** → System shows success + - User redirected to item detail page +``` + +**Edge Case Pattern:** +```markdown +### Edge Case: Network Interruption During Processing + +**Scenario:** User loses connection mid-process +**Expected Behavior:** +1. System retries automatically (3 attempts) +2. If still failing, shows "Processing paused" status +3. User can retry manually when connection restored +**Recovery:** Resume button triggers retry from last checkpoint +``` + +### UI Wireframes Document + +**Purpose:** Page layouts, component specifications, responsive design. + +**ASCII Wireframe:** +``` +### Item List Page + +┌─────────────────────────────────────────────────┐ +│ [Logo] Dashboard Items Settings [User] │ +├─────────────────────────────────────────────────┤ +│ │ +│ Items [+ New Item] │ +│ ───────────────────────────────────────────── │ +│ │ +│ [Search... ] [Filter ▼] [Sort ▼] │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ ○ Item Name Status Created │ │ +│ ├─────────────────────────────────────────┤ │ +│ │ ○ Example Item 1 ● Active Jan 15 │ │ +│ │ ○ Example Item 2 ○ Draft Jan 14 │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ Showing 1-10 of 45 [< 1 2 3 4 5 >] │ +│ │ +└─────────────────────────────────────────────────┘ +``` + +**Component Specification:** +```markdown +### StatusBadge Component + +**Props:** +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| status | string | Yes | 'pending' | 'active' | 'failed' | +| size | string | No | 'sm' | 'md' | 'lg' (default: 'md') | +``` + +--- + +## Cross-Referencing Pattern + +All feature documents should reference each other for easy navigation: + +```markdown +## Related Documentation + +| Document | Description | +|----------|-------------| +| [Planning](../planning/features/item-management.md) | Timeline and decisions | +| [Architecture](../architecture/item-architecture.md) | Technical design | +| [API Contracts](./api-contracts.md) | Endpoint specifications | +| [User Journey](./user-journey.md) | User flows | +| [UI Wireframes](./ui-wireframes.md) | Visual layouts | +``` + +--- + +## Feature Documentation Checklist + +Before implementation begins, verify all documentation is complete: + +### Planning Document +- [ ] Key decisions documented with rationale +- [ ] Implementation phases defined +- [ ] Critical files listed +- [ ] Success criteria measurable + +### Architecture Document +- [ ] System diagram shows all components +- [ ] Database schema complete with indexes +- [ ] API endpoint summary table +- [ ] Error handling strategy defined + +### API Contracts +- [ ] All endpoints documented +- [ ] Request/response examples provided +- [ ] Error codes defined +- [ ] Authentication requirements clear + +### User Journey +- [ ] Personas identified +- [ ] Happy path documented +- [ ] Edge cases considered +- [ ] Error UX defined + +### UI Wireframes +- [ ] All pages wireframed +- [ ] Component props specified +- [ ] Responsive breakpoints defined +- [ ] Navigation integration shown + +--- + +## Design Review Process + +Design reviews validate specifications before implementation, catching issues early when changes are cheap. + +### Review Stages + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Draft │────▶│ Review │────▶│ Approved │────▶│ Implement │ +│ (Author) │ │ (Team) │ │ (Sign-off) │ │ (Dev) │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ │ + │ Iterate │ Request + └───────────────────┘ Changes +``` + +### Stage 1: Self-Review (Author) + +Before sharing, the author verifies: +- [ ] All five document types are complete +- [ ] Cross-references link correctly +- [ ] No placeholder text remains +- [ ] Examples are realistic and consistent + +### Stage 2: Technical Review (Peers) + +**Participants:** 2-3 engineers familiar with affected systems + +**Focus Areas:** + +| Focus | Questions to Answer | +|-------|---------------------| +| Architecture | Does this integrate cleanly? Any conflicts? | +| API Design | Are endpoints RESTful? Consistent patterns? | +| Database | Are indexes sufficient? Migration concerns? | +| Security | Authentication handled? Input validation complete? | +| Performance | Any N+1 queries? Caching strategy needed? | + +### Stage 3: Approval and Sign-off + +**Requirements:** +- All blocking comments resolved +- At least 2 technical approvals +- Stakeholder approval (if applicable) + +**Sign-off Format:** +```markdown +## Approvals + +| Role | Name | Date | Status | +|------|------|------|--------| +| Tech Lead | {Name} | YYYY-MM-DD | Approved | +| Senior Dev | {Name} | YYYY-MM-DD | Approved | +``` + +--- + +## Spec Versioning and Change Management + +### Version Header + +Every spec document should include: + +```markdown +# Feature: Item Management + +**Version:** 1.2.0 +**Status:** Approved +**Last Updated:** 2025-01-15 +**Author:** {Name} + +## Change History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.2.0 | 2025-01-15 | {Name} | Added batch processing endpoint | +| 1.1.0 | 2025-01-10 | {Name} | Updated validation rules | +| 1.0.0 | 2025-01-05 | {Name} | Initial approved version | +``` + +### Semantic Versioning for Specs + +| Version Bump | When to Use | Example | +|--------------|-------------|---------| +| **Major (X.0.0)** | Breaking changes | Removing endpoint, changing auth model | +| **Minor (1.X.0)** | Additive changes | New endpoint, new optional field | +| **Patch (1.0.X)** | Clarifications | Better examples, formatting fixes | + +### Change Request Process + +When specs need to change during implementation: + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Change │────▶│ Impact │────▶│ Review │ +│ Identified │ │ Analysis │ │ & Approve │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ┌─────────────────────────┘ + ▼ + ┌─────────────┐ ┌─────────────┐ + │ Update │────▶│ Notify │ + │ Specs │ │ Team │ + └─────────────┘ └─────────────┘ +``` + +### Document Lifecycle States + +| Status | Meaning | Can Edit? | +|--------|---------|-----------| +| **Draft** | Initial writing | Yes, freely | +| **In Review** | Under team review | Yes, based on feedback | +| **Approved** | Ready for implementation | Only via change request | +| **In Progress** | Implementation underway | Only via change request | +| **Implemented** | Feature shipped | Archive only | +| **Deprecated** | Replaced by newer spec | No changes | + +--- + +## Anti-Patterns to Avoid + +| Anti-Pattern | Problem | Solution | +|--------------|---------|----------| +| Writing docs after implementation | Docs become stale/incomplete | Document during planning phase | +| Single monolithic spec | Hard to navigate and update | Split into focused documents | +| No cross-references | Readers can't find related info | Add Related Documentation section | +| Missing error scenarios | Edge cases discovered in production | Document error handling explicitly | +| Vague success criteria | No way to verify completion | Use measurable, specific goals | +| No file list | Code reviews miss scope | List all files to create/modify | + +--- + +## Quick Reference Card + +### Flat Structure Checklist +- [ ] CLAUDE.md - Project configuration +- [ ] README.md - Quick start and overview +- [ ] plan.md - Tasks and progress +- [ ] architecture.md - System design +- [ ] requirements.md - Feature requirements +- [ ] ui-specs.md - UI specifications (if applicable) + +### Nested Structure Checklist +- [ ] CLAUDE.md - Project configuration +- [ ] README.md - Quick start and overview +- [ ] project/planning/devplan.md - Master plan +- [ ] project/planning/devprogress.md - Progress tracker +- [ ] project/planning/database.md - Schema documentation +- [ ] project/specs/*.md - Feature specifications +- [ ] project/architecture/*.md - Architecture documents +- [ ] project/sessions/*.md - Session summaries + +### File Update Frequency + +| File | Update Frequency | +|------|------------------| +| CLAUDE.md | When conventions change | +| README.md | When setup changes | +| plan.md / devplan.md | Start of each phase | +| devprogress.md | After each task | +| architecture.md | When design changes | +| specs/*.md | Before implementing feature | +| sessions/*.md | End of each session | + +--- + +**Prev:** [Git Workflow](./11-git-workflow.md) | **Next:** [Documentation Writing](./13-documentation-writing.md) diff --git a/docs/13-documentation-writing.md b/docs/13-documentation-writing.md new file mode 100644 index 0000000..f080cd4 --- /dev/null +++ b/docs/13-documentation-writing.md @@ -0,0 +1,556 @@ +# Documentation Writing + +How to write effective documentation for AI-first development projects. + +## Why Documentation Writing Matters + +In AI-first development, documentation serves as: +- **AI context** - Claude reads your docs to understand patterns +- **Team knowledge** - Shared understanding across developers +- **Decision history** - Why choices were made +- **Onboarding material** - New team members get up to speed + +Good documentation is precise, scannable, and actionable. + +## Types of Documentation + +| Type | Purpose | Audience | Update Frequency | +|------|---------|----------|------------------| +| API Documentation | Endpoint contracts | Frontend devs, integrators | Per API change | +| Code Comments | Logic explanation | Future maintainers | With code changes | +| ADRs | Decision rationale | Team, future devs | Per major decision | +| README | Project entry point | All developers | Setup changes | +| Inline Types | Type contracts | All developers | With code changes | + +--- + +## API Documentation + +### OpenAPI/Swagger + +For REST APIs, use OpenAPI specification: + +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 + description: User authentication and management API + +paths: + /api/auth/register: + post: + summary: Register a new user + tags: + - Authentication + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - password + properties: + email: + type: string + format: email + example: user@example.com + password: + type: string + minLength: 8 + example: securepassword123 + responses: + '201': + description: User created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid input + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Email already exists + +components: + schemas: + User: + type: object + properties: + id: + type: integer + email: + type: string + created_at: + type: string + format: date-time + Error: + type: object + properties: + error: + type: string + code: + type: string +``` + +### Markdown API Docs + +For simpler projects, markdown tables work well: + +```markdown +## POST /api/auth/register + +Register a new user account. + +### Request + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| email | string | Yes | Valid email address | +| password | string | Yes | Min 8 characters | + +### Response + +**201 Created** +```json +{ + "id": 1, + "email": "user@example.com", + "created_at": "2025-01-15T10:00:00Z" +} +``` + +**400 Bad Request** +```json +{ + "error": "invalid email format", + "code": "VALIDATION_ERROR" +} +``` + +### Example + +```bash +curl -X POST http://localhost:8080/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"email": "user@example.com", "password": "securepass123"}' +``` +``` + +### Best Practices for API Docs + +**Do:** +- Include request and response examples +- Document all error codes +- Show authentication requirements +- Provide cURL examples +- Keep examples up-to-date + +**Don't:** +- Document internal-only endpoints publicly +- Include sensitive data in examples +- Forget to update after API changes +- Use placeholder values without explaining + +--- + +## Code Comments + +### When to Comment + +**Comment when:** +- Logic is non-obvious +- Business rules are embedded +- Workarounds exist for known issues +- Complex algorithms are used +- External dependencies have quirks + +**Don't comment:** +- Obvious code (`i++ // increment i`) +- Self-explanatory function names +- Every line or function +- Commented-out code (delete it) + +### Comment Patterns + +**Function documentation (Go):** +```go +// CreateUser creates a new user with the given email and password. +// The password is hashed using bcrypt before storage. +// Returns ErrDuplicateEmail if the email already exists. +func (r *UserRepository) CreateUser(ctx context.Context, email, password string) (*User, error) { + // ... +} +``` + +**Function documentation (TypeScript):** +```typescript +/** + * Creates a new user with the given email and password. + * @param email - User's email address (must be unique) + * @param password - Plain text password (will be hashed) + * @returns The created user object + * @throws {DuplicateEmailError} If email already exists + */ +async function createUser(email: string, password: string): Promise { + // ... +} +``` + +**Inline comments for complex logic:** +```go +func calculateDiscount(order Order) float64 { + discount := 0.0 + + // Apply volume discount: 10% off for orders over $100 + if order.Total > 100 { + discount += 0.10 + } + + // Loyalty discount: additional 5% for customers with 10+ orders + // Note: This stacks with volume discount per business requirement #42 + if order.Customer.OrderCount >= 10 { + discount += 0.05 + } + + // Cap total discount at 20% per finance policy + if discount > 0.20 { + discount = 0.20 + } + + return discount +} +``` + +**TODO comments:** +```go +// TODO(username): Implement retry logic for transient failures +// See: https://github.com/org/repo/issues/123 + +// FIXME: This query is slow for large datasets, needs optimization +// Tracked in JIRA-456 + +// HACK: Workaround for library bug, remove after upgrading to v2.0 +// See: https://github.com/lib/issues/789 +``` + +--- + +## Architecture Decision Records (ADRs) + +ADRs document significant technical decisions and their rationale. + +### ADR Template + +```markdown +# ADR-001: Use PostgreSQL for Primary Database + +## Status +Accepted + +## Date +2025-01-15 + +## Context +We need to choose a primary database for our application. The application +requires: +- ACID transactions for financial data +- JSON storage for flexible schemas +- Full-text search capabilities +- Horizontal read scaling + +## Options Considered + +### Option 1: PostgreSQL +**Pros:** +- ACID compliant +- Native JSON/JSONB support +- Full-text search built-in +- Read replicas for scaling +- Mature ecosystem + +**Cons:** +- More complex than SQLite for small projects +- Requires separate server process + +### Option 2: MongoDB +**Pros:** +- Flexible schema +- Built-in horizontal scaling +- Good for document-heavy workloads + +**Cons:** +- No true ACID transactions (until v4.0) +- Different query paradigm +- Less mature tooling for our stack + +### Option 3: MySQL +**Pros:** +- Widely used +- Good performance +- Simple replication + +**Cons:** +- JSON support less mature than PostgreSQL +- Full-text search requires separate engine + +## Decision +We will use **PostgreSQL** as our primary database. + +## Rationale +PostgreSQL best meets our requirements: +1. ACID transactions are critical for financial data +2. JSONB allows flexible schemas without sacrificing query performance +3. Built-in full-text search avoids additional infrastructure +4. The team has PostgreSQL experience + +## Consequences + +### Positive +- Strong data integrity guarantees +- Single database handles JSON and relational data +- Proven scaling patterns available + +### Negative +- Requires PostgreSQL expertise for optimization +- More operational overhead than SQLite +- Team needs to learn PostgreSQL-specific features + +## Related +- ADR-002: Use TimescaleDB extension for time-series data +- ADR-003: Database backup and recovery strategy +``` + +### When to Write ADRs + +Write an ADR when: +- Choosing between technologies (database, framework, library) +- Defining architectural patterns (microservices, monolith) +- Setting standards (API versioning, error handling) +- Making trade-offs (performance vs. simplicity) +- Changing existing patterns + +### ADR File Organization + +``` +docs/ +└── adr/ + ├── README.md # Index of all ADRs + ├── 001-database.md + ├── 002-authentication.md + ├── 003-api-versioning.md + └── template.md # ADR template +``` + +--- + +## Living Documentation + +Documentation that stays in sync with code. + +### Documentation as Code + +**Generate docs from code:** +```bash +# Go - godoc +godoc -http=:6060 + +# TypeScript - TypeDoc +npx typedoc --out docs src/ + +# Python - Sphinx +sphinx-build -b html docs/ docs/_build/ + +# OpenAPI - Generate from annotations +swag init # Go +``` + +**Test documentation examples:** +```go +// Example code in Go tests becomes documentation +func ExampleCreateUser() { + user, err := CreateUser("test@example.com", "password123") + if err != nil { + log.Fatal(err) + } + fmt.Println(user.Email) + // Output: test@example.com +} +``` + +### Documentation Testing + +Ensure documentation stays accurate: + +```yaml +# GitHub Actions workflow +name: Docs +on: [push] +jobs: + test-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Test code examples in markdown + - name: Test markdown code blocks + run: npx markdown-doctest + + # Verify links aren't broken + - name: Check links + run: npx markdown-link-check **/*.md + + # Ensure OpenAPI spec is valid + - name: Validate OpenAPI + run: npx @redocly/cli lint openapi.yaml +``` + +### AI-Assisted Documentation Updates + +Ask Claude to help maintain docs: + +``` +I've added a new endpoint POST /api/orders. Please: +1. Update the API documentation in docs/api.md +2. Add the endpoint to the OpenAPI spec +3. Create a code example for the README + +Here's the handler code: +[paste code] +``` + +--- + +## README Best Practices + +### Essential Sections + +Every README should have: + +1. **Title and description** - What is this? +2. **Quick start** - How to run it? +3. **Prerequisites** - What's needed? +4. **Installation** - Step-by-step setup +5. **Usage** - Basic examples +6. **Documentation links** - Where to learn more + +### README vs Other Docs + +| README | Other Docs | +|--------|------------| +| Quick start | Detailed guides | +| Overview | Deep dives | +| Basic examples | All examples | +| Setup steps | Troubleshooting | +| Links to more | Full content | + +### Keep README Fresh + +```markdown + +## Quick Start + + +```bash +npm install +npm run dev +``` + +Last verified: 2025-01-15 +``` + +--- + +## Documentation Style Guide + +### Writing Style + +**Be direct:** +```markdown +❌ "You might want to consider running the tests" +✅ "Run the tests" + +❌ "It is recommended that users should..." +✅ "Users should..." +``` + +**Use active voice:** +```markdown +❌ "The configuration file is read by the server" +✅ "The server reads the configuration file" +``` + +**Be specific:** +```markdown +❌ "Set the timeout to an appropriate value" +✅ "Set the timeout to 30 seconds" +``` + +### Formatting Conventions + +**Headings:** +- `#` for page title (one per doc) +- `##` for main sections +- `###` for subsections +- Don't skip levels + +**Code blocks:** +- Always specify language +- Keep examples runnable +- Show expected output + +**Lists:** +- Use bullets for unordered items +- Use numbers for sequences +- Keep items parallel in structure + +--- + +## Prompting Claude for Documentation + +### Generating API Docs +``` +Generate OpenAPI 3.0 documentation for this Go handler: +[paste handler code] + +Include: +- All request parameters +- All response codes +- Example requests and responses +- Authentication requirements +``` + +### Updating Documentation +``` +The codebase has changed. Please update the documentation: + +Changes made: +- Added rate limiting to /api/auth endpoints +- Changed password minimum from 6 to 8 characters +- Added new field 'phone' to user registration + +Files to update: +- docs/api.md +- README.md +``` + +### Creating ADRs +``` +I need to write an ADR for choosing between REST and GraphQL for our API. + +Context: +- Team has REST experience +- Frontend needs flexible queries +- Performance is important + +Please write a complete ADR evaluating both options. +``` + +--- + +**Prev:** [Documentation Organization](./12-documentation-organization.md) | **Next:** [Code Review](./14-code-review.md) diff --git a/docs/14-code-review.md b/docs/14-code-review.md new file mode 100644 index 0000000..cd94893 --- /dev/null +++ b/docs/14-code-review.md @@ -0,0 +1,475 @@ +# Code Review Best Practices + +How to effectively review code in AI-first development where AI generates code and humans validate. + +## The Three-Layer Review Model + +In AI-first development, code passes through three review layers: + +``` +┌─────────────────────────────────────────┐ +│ Layer 1: Self-Review (Developer) │ +│ - Read what AI generated │ +│ - Check it matches requirements │ +│ - Verify tests pass │ +└─────────────────┬───────────────────────┘ + ▼ +┌─────────────────────────────────────────┐ +│ Layer 2: Automated Review (CI) │ +│ - Linting and formatting │ +│ - Test execution │ +│ - Security scanning │ +│ - Coverage thresholds │ +└─────────────────┬───────────────────────┘ + ▼ +┌─────────────────────────────────────────┐ +│ Layer 3: Peer Review (Team) │ +│ - Architecture alignment │ +│ - Business logic correctness │ +│ - Edge cases and error handling │ +│ - Knowledge sharing │ +└─────────────────────────────────────────┘ +``` + +## Layer 1: Self-Review + +Before submitting any PR, the developer (human) must review AI-generated code. + +### Self-Review Checklist + +**Understanding:** +- [ ] I understand what every line does +- [ ] I can explain the logic to someone else +- [ ] The code matches the specification + +**Correctness:** +- [ ] The code does what was requested +- [ ] Edge cases are handled +- [ ] Error handling is appropriate +- [ ] No obvious bugs + +**Quality:** +- [ ] Code follows project conventions +- [ ] No unnecessary complexity +- [ ] No hardcoded values that should be configurable +- [ ] Tests are meaningful (not just for coverage) + +**Security:** +- [ ] No secrets in code +- [ ] Input is validated +- [ ] No SQL injection vulnerabilities +- [ ] Authentication/authorization is correct + +### Common AI Code Issues to Catch + +**1. Over-engineering:** +```go +// AI sometimes creates unnecessary abstractions +// ❌ Over-engineered +type UserServiceInterface interface { + CreateUser(ctx context.Context, req CreateUserRequest) (*CreateUserResponse, error) +} + +type UserServiceImpl struct { + repo UserRepositoryInterface +} + +func NewUserService(repo UserRepositoryInterface) UserServiceInterface { + return &UserServiceImpl{repo: repo} +} + +// ✅ Simpler when you only have one implementation +type UserService struct { + repo *UserRepository +} + +func NewUserService(repo *UserRepository) *UserService { + return &UserService{repo: repo} +} +``` + +**2. Missing error context:** +```go +// ❌ AI might return bare errors +if err != nil { + return err +} + +// ✅ Add context +if err != nil { + return fmt.Errorf("failed to create user: %w", err) +} +``` + +**3. Incomplete validation:** +```go +// ❌ AI might miss edge cases +func CreateUser(email string, password string) error { + // Missing: email format validation + // Missing: password strength validation + // Missing: duplicate email check +} + +// ✅ Complete validation +func CreateUser(email string, password string) error { + if !isValidEmail(email) { + return ErrInvalidEmail + } + if len(password) < 8 { + return ErrWeakPassword + } + if exists, _ := userExists(email); exists { + return ErrDuplicateEmail + } + // ... +} +``` + +**4. Hardcoded values:** +```go +// ❌ Hardcoded configuration +token, _ := jwt.Sign(claims, "my-secret-key") +time.Sleep(5 * time.Second) + +// ✅ Configurable +token, _ := jwt.Sign(claims, config.JWTSecret) +time.Sleep(config.RetryDelay) +``` + +--- + +## Layer 2: Automated Review + +Automated checks catch issues before human review. + +### Essential Automated Checks + +**Linting:** +```yaml +# .github/workflows/ci.yml +- name: Lint Go + run: golangci-lint run + +- name: Lint TypeScript + run: npm run lint + +- name: Lint Python + run: ruff check . +``` + +**Formatting:** +```yaml +- name: Check formatting + run: | + go fmt ./... + git diff --exit-code # Fail if changes +``` + +**Tests:** +```yaml +- name: Run tests + run: go test -race -coverprofile=coverage.out ./... + +- name: Check coverage + run: | + coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') + if (( $(echo "$coverage < 70" | bc -l) )); then + echo "Coverage $coverage% is below 70% threshold" + exit 1 + fi +``` + +**Security scanning:** +```yaml +- name: Security scan + run: gosec ./... + +- name: Dependency audit + run: npm audit --audit-level=high +``` + +### Pre-commit Hooks + +Run checks locally before pushing: + +```yaml +# .pre-commit-config.yaml +repos: + - repo: local + hooks: + - id: go-fmt + name: Go format + entry: go fmt ./... + language: system + pass_filenames: false + + - id: go-test + name: Go test + entry: go test ./... + language: system + pass_filenames: false + + - id: go-vet + name: Go vet + entry: go vet ./... + language: system + pass_filenames: false +``` + +--- + +## Layer 3: Peer Review + +Human review focuses on what automation can't catch. + +### What Reviewers Should Check + +**1. Architecture alignment:** +- Does this follow our established patterns? +- Is this the right place for this code? +- Does it fit with our overall design? + +**2. Business logic:** +- Does this correctly implement the requirements? +- Are there business edge cases not covered? +- Is the behavior correct for all user types? + +**3. Maintainability:** +- Will future developers understand this? +- Is there unnecessary complexity? +- Are there opportunities to reuse existing code? + +**4. Performance implications:** +- Are there N+1 query issues? +- Is there unnecessary data loading? +- Could this become slow at scale? + +### Review Comment Guidelines + +**Be specific:** +```markdown +❌ "This could be better" +✅ "Consider using a map here instead of a slice for O(1) lookup" +``` + +**Explain why:** +```markdown +❌ "Don't use string concatenation in a loop" +✅ "String concatenation in a loop creates many allocations. + Use strings.Builder for better performance." +``` + +**Suggest solutions:** +```markdown +❌ "This validation is incomplete" +✅ "This validation is missing the case where email is empty. + Consider: `if email == "" { return ErrEmptyEmail }`" +``` + +**Distinguish severity:** +```markdown +🔴 BLOCKER: Security issue - user input not sanitized +🟡 SHOULD FIX: Performance issue - N+1 queries +🟢 SUGGESTION: Consider extracting this to a helper function +❓ QUESTION: Why did we choose this approach over X? +``` + +### PR Size Guidelines + +| Size | Lines Changed | Review Time | Guidance | +|------|--------------|-------------|----------| +| XS | < 50 | 5-10 min | Quick review | +| S | 50-200 | 15-30 min | Standard review | +| M | 200-500 | 30-60 min | Detailed review | +| L | 500-1000 | 1-2 hours | Consider splitting | +| XL | > 1000 | Too long | Must split | + +**Large PRs should be split by:** +- Layer (database, API, tests separately) +- Feature (one PR per sub-feature) +- Refactor vs. feature (separate PRs) + +--- + +## AI-Generated Code Review Checklist + +Use this checklist specifically for AI-generated code: + +### Functionality +- [ ] Code implements the requested feature correctly +- [ ] All acceptance criteria are met +- [ ] Edge cases are handled (empty inputs, nulls, boundaries) +- [ ] Error messages are helpful and user-friendly + +### Code Quality +- [ ] Code follows project style guidelines +- [ ] Variable names are descriptive and consistent +- [ ] No dead code or commented-out code +- [ ] Functions are focused and not too long (< 50 lines) +- [ ] No unnecessary abstractions + +### Testing +- [ ] Tests exist and are meaningful +- [ ] Tests cover happy path and error cases +- [ ] Tests are not just achieving coverage numbers +- [ ] Test names describe what they test + +### Security +- [ ] No hardcoded secrets or credentials +- [ ] User input is validated and sanitized +- [ ] SQL queries use parameterized statements +- [ ] Authentication is checked where needed +- [ ] Sensitive data is not logged + +### Performance +- [ ] No obvious N+1 query problems +- [ ] Database queries are efficient (indexes used) +- [ ] No unnecessary memory allocations +- [ ] Pagination is used for large datasets + +### Documentation +- [ ] Public APIs are documented +- [ ] Complex logic has explanatory comments +- [ ] README is updated if needed +- [ ] CHANGELOG is updated + +--- + +## Review Workflow + +### Standard PR Flow + +``` +1. Developer creates PR + ↓ +2. Automated checks run (CI) + ↓ (pass) +3. Request review from team member + ↓ +4. Reviewer examines code + ↓ +5. Reviewer leaves comments + ↓ (if changes needed) +6. Developer addresses feedback + ↓ (loop until approved) +7. Reviewer approves + ↓ +8. Developer merges (squash) +``` + +### Review Response Time + +| Priority | Response Time | Merge Time | +|----------|--------------|------------| +| Critical hotfix | < 1 hour | Same day | +| Normal feature | < 24 hours | 2-3 days | +| Refactoring | < 48 hours | 1 week | +| Documentation | < 48 hours | 1 week | + +### Handling Review Feedback + +**As the author:** +- Respond to all comments +- Don't take feedback personally +- Ask for clarification if unclear +- Mark resolved comments as resolved + +**As the reviewer:** +- Be constructive, not critical +- Acknowledge good work +- Approve when ready, don't delay +- Follow up on your comments + +--- + +## Reviewing AI-Generated Tests + +Tests from AI need special attention: + +### Watch for Weak Tests + +```javascript +// ❌ Test that always passes +test('user is created', () => { + const user = createUser('test@example.com'); + expect(user).toBeDefined(); // Too weak +}); + +// ✅ Test with meaningful assertions +test('user is created with correct properties', () => { + const user = createUser('test@example.com', 'password123'); + expect(user.email).toBe('test@example.com'); + expect(user.passwordHash).not.toBe('password123'); + expect(user.createdAt).toBeInstanceOf(Date); +}); +``` + +### Watch for Missing Error Tests + +```javascript +// AI often forgets error cases +describe('createUser', () => { + test('creates user successfully', () => { /* ... */ }); + + // ❌ Missing error tests + // ✅ Add these: + test('rejects invalid email', () => { /* ... */ }); + test('rejects weak password', () => { /* ... */ }); + test('rejects duplicate email', () => { /* ... */ }); +}); +``` + +### Watch for Hardcoded Test Data + +```javascript +// ❌ Hardcoded IDs that might conflict +test('gets user by id', async () => { + const user = await getUserById(1); // ID 1 might not exist + expect(user).toBeDefined(); +}); + +// ✅ Create test data +test('gets user by id', async () => { + const created = await createUser('test@example.com'); + const user = await getUserById(created.id); + expect(user.email).toBe('test@example.com'); +}); +``` + +--- + +## Code Review Tools + +### GitHub Features + +- **Required reviews:** Enforce minimum reviewers +- **Code owners:** Auto-assign relevant reviewers +- **Branch protection:** Require CI pass and reviews +- **Review comments:** Inline code discussions + +### Additional Tools + +| Tool | Purpose | +|------|---------| +| `danger` | Automated PR checks and comments | +| `reviewdog` | Post linter results as PR comments | +| `codecov` | Coverage tracking and PR comments | +| `sonarqube` | Code quality and security analysis | + +--- + +## Summary + +**Key Principles:** +- Three layers: Self, Automated, Peer +- AI code needs extra scrutiny +- Keep PRs small and focused +- Be constructive in feedback +- Reviewers catch what automation can't + +**Self-Review First:** +Always review AI-generated code yourself before requesting peer review. You're responsible for understanding and vouching for the code. + +--- + +**Prev:** [Documentation Writing](./13-documentation-writing.md) | **Next:** [Security Practices](./15-security.md) diff --git a/docs/15-security.md b/docs/15-security.md new file mode 100644 index 0000000..469e835 --- /dev/null +++ b/docs/15-security.md @@ -0,0 +1,540 @@ +# Security Practices + +Security considerations for AI-first development and protecting code generated by AI. + +## Why AI Code Needs Security Review + +AI-generated code can have security blind spots: +- May not know your specific security requirements +- Can introduce common vulnerabilities if not prompted correctly +- Might use outdated security patterns +- May not consider your deployment environment + +**Every piece of AI-generated code should be reviewed for security.** + +--- + +## Authentication & Authorization + +### JWT Best Practices + +**Token structure:** +```go +type Claims struct { + UserID int `json:"user_id"` + Email string `json:"email"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func GenerateToken(user *User) (string, error) { + claims := Claims{ + UserID: user.ID, + Email: user.Email, + Role: user.Role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "my-api", + Subject: strconv.Itoa(user.ID), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(os.Getenv("JWT_SECRET"))) +} +``` + +**Token validation:** +```go +func ValidateToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + // Verify signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("JWT_SECRET")), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, ErrInvalidToken +} +``` + +**Security checklist for JWT:** +- [ ] Use strong secret (256+ bits) +- [ ] Set reasonable expiration (hours, not days) +- [ ] Validate signing algorithm +- [ ] Include user ID, not sensitive data +- [ ] Use HTTPS only +- [ ] Implement token refresh +- [ ] Consider token revocation strategy + +### Password Handling + +**Hashing:** +```go +import "golang.org/x/crypto/bcrypt" + +const bcryptCost = 12 // Adjust based on your server capacity + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) + return string(bytes), err +} + +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} +``` + +**Password requirements:** +```go +func ValidatePassword(password string) error { + if len(password) < 8 { + return errors.New("password must be at least 8 characters") + } + + hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password) + hasLower := regexp.MustCompile(`[a-z]`).MatchString(password) + hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password) + + if !hasUpper || !hasLower || !hasNumber { + return errors.New("password must contain uppercase, lowercase, and number") + } + + return nil +} +``` + +### Role-Based Access Control (RBAC) + +**Define permissions:** +```go +type Permission string + +const ( + PermUserRead Permission = "user:read" + PermUserWrite Permission = "user:write" + PermAdminAll Permission = "admin:*" +) + +type Role struct { + Name string + Permissions []Permission +} + +var Roles = map[string]Role{ + "user": { + Name: "user", + Permissions: []Permission{PermUserRead}, + }, + "admin": { + Name: "admin", + Permissions: []Permission{PermUserRead, PermUserWrite, PermAdminAll}, + }, +} +``` + +**Middleware:** +```go +func RequirePermission(required Permission) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims := r.Context().Value("claims").(*Claims) + role := Roles[claims.Role] + + if !hasPermission(role.Permissions, required) { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) + } +} +``` + +--- + +## Input Validation & Sanitization + +### SQL Injection Prevention + +**Always use parameterized queries:** +```go +// ❌ NEVER do this - SQL injection vulnerability +query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email) +rows, err := db.Query(query) + +// ✅ Always use parameterized queries +query := "SELECT * FROM users WHERE email = $1" +rows, err := db.Query(query, email) + +// ✅ Or use query builder +user, err := db.User. + Query(). + Where(user.EmailEQ(email)). + Only(ctx) +``` + +### XSS Prevention + +**Escape output:** +```go +import "html" + +// In templates, use auto-escaping +// Go's html/template does this automatically + +// For manual escaping: +safeHTML := html.EscapeString(userInput) +``` + +**Content Security Policy:** +```go +func SecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + next.ServeHTTP(w, r) + }) +} +``` + +### Request Validation + +**Validate all inputs:** +```go +type CreateUserRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` + Name string `json:"name" validate:"required,max=100"` +} + +func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { + var req CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "invalid JSON") + return + } + + // Validate struct + if err := h.validator.Struct(req); err != nil { + respondError(w, http.StatusBadRequest, formatValidationErrors(err)) + return + } + + // Sanitize inputs + req.Email = strings.ToLower(strings.TrimSpace(req.Email)) + req.Name = strings.TrimSpace(req.Name) + + // ... proceed with validated, sanitized data +} +``` + +--- + +## Secrets Management + +### Environment Variables + +**Never commit secrets:** +```bash +# .gitignore +.env +.env.local +.env.*.local +*.pem +*.key +credentials.json +``` + +**Use .env.example:** +```bash +# .env.example (commit this) +DATABASE_URL=postgres://user:pass@localhost:5432/dbname +JWT_SECRET=your-secret-here-min-32-chars +API_KEY=your-api-key +``` + +**Load securely:** +```go +func LoadConfig() (*Config, error) { + // In development, load from .env + if os.Getenv("ENV") != "production" { + godotenv.Load() + } + + config := &Config{ + DatabaseURL: os.Getenv("DATABASE_URL"), + JWTSecret: os.Getenv("JWT_SECRET"), + } + + // Validate required config + if config.JWTSecret == "" { + return nil, errors.New("JWT_SECRET is required") + } + if len(config.JWTSecret) < 32 { + return nil, errors.New("JWT_SECRET must be at least 32 characters") + } + + return config, nil +} +``` + +### Secret Scanning + +**Pre-commit hook:** +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + args: ['--baseline', '.secrets.baseline'] +``` + +**CI scanning:** +```yaml +# GitHub Actions +- name: Scan for secrets + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} +``` + +### Production Secrets + +**Use secret management services:** +- AWS Secrets Manager +- Google Secret Manager +- HashiCorp Vault +- Kubernetes Secrets + +```go +// Example: AWS Secrets Manager +func GetSecret(secretName string) (string, error) { + cfg, _ := config.LoadDefaultConfig(context.TODO()) + client := secretsmanager.NewFromConfig(cfg) + + result, err := client.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(secretName), + }) + if err != nil { + return "", err + } + + return *result.SecretString, nil +} +``` + +--- + +## API Security + +### Rate Limiting + +```go +import "golang.org/x/time/rate" + +type RateLimiter struct { + visitors map[string]*rate.Limiter + mu sync.RWMutex + rate rate.Limit + burst int +} + +func NewRateLimiter(r rate.Limit, b int) *RateLimiter { + return &RateLimiter{ + visitors: make(map[string]*rate.Limiter), + rate: r, + burst: b, + } +} + +func (rl *RateLimiter) GetLimiter(ip string) *rate.Limiter { + rl.mu.Lock() + defer rl.mu.Unlock() + + limiter, exists := rl.visitors[ip] + if !exists { + limiter = rate.NewLimiter(rl.rate, rl.burst) + rl.visitors[ip] = limiter + } + + return limiter +} + +func RateLimitMiddleware(rl *RateLimiter) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip := getIP(r) + limiter := rl.GetLimiter(ip) + + if !limiter.Allow() { + http.Error(w, "rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) + } +} +``` + +### CORS Configuration + +```go +func CORSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only allow specific origins in production + allowedOrigins := map[string]bool{ + "https://myapp.com": true, + "https://www.myapp.com": true, + } + + origin := r.Header.Get("Origin") + if allowedOrigins[origin] { + w.Header().Set("Access-Control-Allow-Origin", origin) + } + + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Max-Age", "86400") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +### Error Message Security + +```go +// ❌ Don't leak internal details +func handleError(w http.ResponseWriter, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + // Exposes: "pq: duplicate key value violates unique constraint" +} + +// ✅ Return safe error messages +func handleError(w http.ResponseWriter, err error) { + // Log the real error internally + log.Printf("error: %v", err) + + // Return generic message to client + var apiErr *APIError + if errors.As(err, &apiErr) { + respondJSON(w, apiErr.StatusCode, map[string]string{ + "error": apiErr.Message, + "code": apiErr.Code, + }) + return + } + + respondJSON(w, http.StatusInternalServerError, map[string]string{ + "error": "internal server error", + "code": "INTERNAL_ERROR", + }) +} +``` + +--- + +## Security Checklist for PRs + +Use this checklist when reviewing AI-generated code: + +### Authentication +- [ ] No hardcoded credentials +- [ ] Passwords are hashed (bcrypt, argon2) +- [ ] JWT tokens expire appropriately +- [ ] Token validation checks algorithm +- [ ] Session tokens are invalidated on logout + +### Authorization +- [ ] All endpoints check permissions +- [ ] Users can only access their own data +- [ ] Admin functions require admin role +- [ ] API keys have appropriate scopes + +### Input Validation +- [ ] All user input is validated +- [ ] SQL queries are parameterized +- [ ] File uploads are restricted +- [ ] JSON parsing has limits +- [ ] Path traversal is prevented + +### Output +- [ ] HTML is escaped +- [ ] Errors don't leak internal info +- [ ] Security headers are set +- [ ] Sensitive data isn't logged + +### Secrets +- [ ] No secrets in code or commits +- [ ] Secrets loaded from environment +- [ ] .env files are gitignored +- [ ] Production uses secret manager + +### Dependencies +- [ ] Dependencies are from trusted sources +- [ ] No known vulnerable versions +- [ ] Lock files are committed + +--- + +## Prompting Claude for Secure Code + +**When requesting authentication:** +``` +Implement user authentication with: +- Password hashing using bcrypt (cost factor 12) +- JWT tokens with 24-hour expiry +- Token refresh mechanism +- Rate limiting on auth endpoints (10 requests/minute) +- Secure password requirements (min 8 chars, mixed case, numbers) + +Do NOT: +- Store plain text passwords +- Use hardcoded secrets +- Return detailed error messages to clients +``` + +**When requesting API endpoints:** +``` +Create a CRUD API for [resource] with: +- Input validation for all fields +- Parameterized SQL queries only +- Permission checks on each endpoint +- Rate limiting +- Pagination for list endpoints + +Security requirements: +- Sanitize all string inputs +- Validate IDs are integers +- Check user owns the resource before update/delete +``` + +--- + +**Prev:** [Code Review](./14-code-review.md) | **Next:** [Performance Optimization](./16-performance.md) diff --git a/docs/16-performance.md b/docs/16-performance.md new file mode 100644 index 0000000..3b38417 --- /dev/null +++ b/docs/16-performance.md @@ -0,0 +1,601 @@ +# Performance Optimization + +How to identify and resolve performance issues in AI-first development. + +## Performance-First Mindset + +**Core principle:** Measure before optimizing. + +``` +1. Define performance goals +2. Measure current state +3. Identify bottlenecks +4. Optimize the bottleneck +5. Measure again +6. Repeat +``` + +**Don't:** +- Optimize without measuring +- Assume you know where the problem is +- Optimize everything at once +- Sacrifice readability for micro-optimizations + +--- + +## Performance Budgets + +Set clear targets before implementation: + +| Metric | Target | Measurement | +|--------|--------|-------------| +| API response (p50) | < 100ms | Server-side | +| API response (p95) | < 500ms | Server-side | +| Page load (FCP) | < 1.5s | Lighthouse | +| Page load (LCP) | < 2.5s | Lighthouse | +| Time to Interactive | < 3.5s | Lighthouse | +| Bundle size (JS) | < 200KB gzip | Build output | + +--- + +## Database Performance + +### Query Optimization + +**Identify slow queries:** +```sql +-- PostgreSQL: Find slow queries +SELECT query, calls, mean_time, total_time +FROM pg_stat_statements +ORDER BY mean_time DESC +LIMIT 10; + +-- Enable slow query logging +ALTER SYSTEM SET log_min_duration_statement = 1000; -- Log queries > 1s +``` + +**Common optimizations:** + +**1. Add indexes:** +```sql +-- Before: Full table scan +SELECT * FROM users WHERE email = 'user@example.com'; +-- Query time: 500ms (1M rows) + +-- After: Index scan +CREATE INDEX idx_users_email ON users(email); +-- Query time: 1ms +``` + +**2. Use composite indexes for multi-column queries:** +```sql +-- Query pattern +SELECT * FROM orders WHERE user_id = 1 AND status = 'pending'; + +-- Single column index - partially effective +CREATE INDEX idx_orders_user_id ON orders(user_id); + +-- Composite index - optimal for this query +CREATE INDEX idx_orders_user_status ON orders(user_id, status); +``` + +**3. Avoid SELECT *:** +```go +// ❌ Fetches all columns +rows, _ := db.Query("SELECT * FROM users WHERE id = $1", id) + +// ✅ Fetch only needed columns +rows, _ := db.Query("SELECT id, email, name FROM users WHERE id = $1", id) +``` + +### N+1 Query Problem + +The most common performance issue in AI-generated code. + +**Problem:** +```go +// Fetches all orders +orders, _ := db.Query("SELECT * FROM orders") + +for orders.Next() { + var order Order + orders.Scan(&order.ID, &order.UserID, ...) + + // N additional queries! + user, _ := db.QueryRow("SELECT * FROM users WHERE id = $1", order.UserID) +} +// 1 + N queries total +``` + +**Solution - JOIN:** +```go +query := ` + SELECT o.id, o.total, u.id, u.name, u.email + FROM orders o + JOIN users u ON u.id = o.user_id +` +rows, _ := db.Query(query) +// 1 query total +``` + +**Solution - Batch load:** +```go +// Get all orders +orders, _ := getOrders() + +// Collect unique user IDs +userIDs := make(map[int]bool) +for _, o := range orders { + userIDs[o.UserID] = true +} + +// Single query for all users +users, _ := getUsersByIDs(keys(userIDs)) +// 2 queries total (regardless of order count) +``` + +### Connection Pooling + +```go +import "database/sql" + +func SetupDB() *sql.DB { + db, _ := sql.Open("postgres", connectionString) + + // Configure pool + db.SetMaxOpenConns(25) // Max concurrent connections + db.SetMaxIdleConns(5) // Idle connections to keep + db.SetConnMaxLifetime(5 * time.Minute) // Recycle connections + + return db +} +``` + +### Caching Strategies + +**Application-level cache:** +```go +import "github.com/patrickmn/go-cache" + +var userCache = cache.New(5*time.Minute, 10*time.Minute) + +func GetUser(id int) (*User, error) { + // Check cache first + key := fmt.Sprintf("user:%d", id) + if cached, found := userCache.Get(key); found { + return cached.(*User), nil + } + + // Cache miss - fetch from DB + user, err := db.GetUser(id) + if err != nil { + return nil, err + } + + // Store in cache + userCache.Set(key, user, cache.DefaultExpiration) + return user, nil +} +``` + +**Redis cache:** +```go +func GetUser(ctx context.Context, id int) (*User, error) { + key := fmt.Sprintf("user:%d", id) + + // Try cache + data, err := redis.Get(ctx, key).Bytes() + if err == nil { + var user User + json.Unmarshal(data, &user) + return &user, nil + } + + // Cache miss + user, err := db.GetUser(id) + if err != nil { + return nil, err + } + + // Cache for 5 minutes + data, _ := json.Marshal(user) + redis.Set(ctx, key, data, 5*time.Minute) + + return user, nil +} +``` + +**Cache invalidation:** +```go +func UpdateUser(ctx context.Context, user *User) error { + // Update database + if err := db.UpdateUser(user); err != nil { + return err + } + + // Invalidate cache + key := fmt.Sprintf("user:%d", user.ID) + redis.Del(ctx, key) + + return nil +} +``` + +--- + +## API Performance + +### Response Time Optimization + +**1. Pagination:** +```go +type PaginatedResponse struct { + Data []Item `json:"data"` + Meta Meta `json:"meta"` +} + +type Meta struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + TotalItems int `json:"total_items"` + TotalPages int `json:"total_pages"` +} + +func GetItems(page, perPage int) (*PaginatedResponse, error) { + // Limit per_page to prevent abuse + if perPage > 100 { + perPage = 100 + } + + offset := (page - 1) * perPage + + items, _ := db.Query(` + SELECT * FROM items + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + `, perPage, offset) + + total, _ := db.QueryRow("SELECT COUNT(*) FROM items") + + return &PaginatedResponse{ + Data: items, + Meta: Meta{ + Page: page, + PerPage: perPage, + TotalItems: total, + TotalPages: (total + perPage - 1) / perPage, + }, + }, nil +} +``` + +**2. Field selection:** +```go +// Allow clients to request specific fields +// GET /api/users?fields=id,name,email + +func GetUsers(fields []string) ([]map[string]interface{}, error) { + allowedFields := map[string]bool{ + "id": true, "name": true, "email": true, "created_at": true, + } + + // Validate and build SELECT clause + selectFields := []string{} + for _, f := range fields { + if allowedFields[f] { + selectFields = append(selectFields, f) + } + } + + if len(selectFields) == 0 { + selectFields = []string{"id", "name", "email"} // Default + } + + query := fmt.Sprintf("SELECT %s FROM users", strings.Join(selectFields, ", ")) + // ... +} +``` + +**3. Compression:** +```go +import "github.com/go-chi/chi/middleware" + +router := chi.NewRouter() +router.Use(middleware.Compress(5)) // gzip compression level 5 +``` + +### Async Processing + +Move slow operations out of the request cycle: + +```go +// ❌ Synchronous - slow response +func CreateOrder(w http.ResponseWriter, r *http.Request) { + order := createOrder(r) + + sendConfirmationEmail(order) // 500ms + updateInventory(order) // 200ms + notifyWarehouse(order) // 300ms + + respondJSON(w, http.StatusCreated, order) + // Total: 1000ms+ +} + +// ✅ Async - fast response +func CreateOrder(w http.ResponseWriter, r *http.Request) { + order := createOrder(r) + + // Queue background tasks + queue.Publish("order.created", order) + + respondJSON(w, http.StatusCreated, order) + // Total: ~100ms +} + +// Background worker handles slow tasks +func ProcessOrderCreated(order Order) { + sendConfirmationEmail(order) + updateInventory(order) + notifyWarehouse(order) +} +``` + +--- + +## Frontend Performance + +### Bundle Size Optimization + +**1. Code splitting:** +```javascript +// ❌ Everything in one bundle +import { Dashboard } from './Dashboard'; +import { Settings } from './Settings'; +import { Admin } from './Admin'; + +// ✅ Lazy load routes +const Dashboard = React.lazy(() => import('./Dashboard')); +const Settings = React.lazy(() => import('./Settings')); +const Admin = React.lazy(() => import('./Admin')); + +function App() { + return ( + }> + + } /> + } /> + } /> + + + ); +} +``` + +**2. Tree shaking:** +```javascript +// ❌ Import entire library +import _ from 'lodash'; +const result = _.map(items, 'name'); + +// ✅ Import only what you need +import map from 'lodash/map'; +const result = map(items, 'name'); +``` + +**3. Analyze bundle:** +```bash +# Webpack bundle analyzer +npm install webpack-bundle-analyzer +npx webpack-bundle-analyzer dist/stats.json + +# Vite +npm run build -- --stats +``` + +### Image Optimization + +```javascript +// Use responsive images +Description + +// Or use Next.js Image component +import Image from 'next/image'; + +Description +``` + +### Virtual Scrolling + +For long lists (100+ items): + +```javascript +import { FixedSizeList } from 'react-window'; + +function VirtualList({ items }) { + const Row = ({ index, style }) => ( +
+ {items[index].name} +
+ ); + + return ( + + {Row} + + ); +} +``` + +--- + +## Monitoring & Profiling + +### Application Metrics + +```go +import "github.com/prometheus/client_golang/prometheus" + +var ( + requestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration", + Buckets: []float64{.01, .05, .1, .25, .5, 1, 2.5, 5}, + }, + []string{"method", "path", "status"}, + ) + + requestCount = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total HTTP requests", + }, + []string{"method", "path", "status"}, + ) +) + +func MetricsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Wrap response writer to capture status + ww := &responseWriter{ResponseWriter: w, status: 200} + next.ServeHTTP(ww, r) + + duration := time.Since(start).Seconds() + labels := prometheus.Labels{ + "method": r.Method, + "path": r.URL.Path, + "status": strconv.Itoa(ww.status), + } + + requestDuration.With(labels).Observe(duration) + requestCount.With(labels).Inc() + }) +} +``` + +### Profiling Go Applications + +```go +import _ "net/http/pprof" + +func main() { + // pprof endpoints available at /debug/pprof/ + go http.ListenAndServe(":6060", nil) + + // Your application + startServer() +} +``` + +```bash +# CPU profile +go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 + +# Memory profile +go tool pprof http://localhost:6060/debug/pprof/heap + +# Goroutine profile +go tool pprof http://localhost:6060/debug/pprof/goroutine +``` + +### Performance Testing + +```bash +# Load testing with hey +hey -n 10000 -c 100 http://localhost:8080/api/users + +# Output: +# Summary: +# Total: 5.2341 secs +# Slowest: 0.5234 secs +# Fastest: 0.0012 secs +# Average: 0.0523 secs +# Requests/sec: 1910.5234 +``` + +--- + +## Prompting Claude for Performance + +**For database optimization:** +``` +Optimize this database query for performance: +[paste query] + +Context: +- Table has 1M+ rows +- Query runs frequently (100+ times/minute) +- Current execution time: 500ms + +Please: +1. Analyze the query plan +2. Suggest appropriate indexes +3. Rewrite the query if needed +4. Estimate improvement +``` + +**For API performance:** +``` +This API endpoint is slow (2s average response): +[paste handler code] + +Please: +1. Identify performance bottlenecks +2. Suggest N+1 query fixes +3. Add caching where appropriate +4. Consider async processing for slow operations + +Target: < 200ms response time +``` + +--- + +## Performance Checklist + +### Database +- [ ] Queries have appropriate indexes +- [ ] No N+1 query patterns +- [ ] Large result sets are paginated +- [ ] Connection pooling is configured +- [ ] Slow queries are logged and monitored + +### API +- [ ] Response times are monitored +- [ ] Large responses use pagination +- [ ] Compression is enabled +- [ ] Slow operations are async +- [ ] Caching is used for frequent reads + +### Frontend +- [ ] Bundle size is monitored +- [ ] Routes are code-split +- [ ] Images are optimized and lazy-loaded +- [ ] Long lists use virtualization +- [ ] Core Web Vitals meet targets + +--- + +**Prev:** [Security Practices](./15-security.md) | **Next:** [CI/CD and Deployment](./17-ci-cd.md) diff --git a/docs/17-ci-cd.md b/docs/17-ci-cd.md new file mode 100644 index 0000000..559d5bc --- /dev/null +++ b/docs/17-ci-cd.md @@ -0,0 +1,715 @@ +# CI/CD and Deployment + +Setting up continuous integration and deployment pipelines for AI-first development. + +## CI/CD Philosophy + +In AI-first development, automated pipelines are essential: +- **Every commit is tested** - Catch issues early +- **Fast feedback** - Know within minutes if something is broken +- **Consistent quality** - Same checks every time +- **Safe deployment** - Automated, repeatable process + +--- + +## GitHub Actions Setup + +### Basic Workflow Structure + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go test -v ./... +``` + +### Complete Go CI Pipeline + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +env: + GO_VERSION: '1.21' + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + + test: + name: Test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: testdb + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run tests with coverage + env: + DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable + run: | + go test -race -coverprofile=coverage.out -covermode=atomic ./... + + - name: Check coverage threshold + run: | + coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') + echo "Coverage: $coverage%" + if (( $(echo "$coverage < 70" | bc -l) )); then + echo "Coverage $coverage% is below 70% threshold" + exit 1 + fi + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + files: coverage.out + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build + run: go build -o bin/server ./cmd/server + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: server + path: bin/server + + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run Gosec + uses: securego/gosec@master + with: + args: ./... + + - name: Run Trivy + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' +``` + +### Node.js/TypeScript CI Pipeline + +```yaml +# .github/workflows/ci.yml +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Type check + run: npm run type-check + + - name: Run tests + run: npm test -- --coverage + + - name: Check coverage + run: | + coverage=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') + echo "Coverage: $coverage%" + if (( $(echo "$coverage < 70" | bc -l) )); then + echo "Coverage below threshold" + exit 1 + fi + + e2e: + runs-on: ubuntu-latest + needs: lint-and-test + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Playwright + run: npx playwright install --with-deps + + - name: Run E2E tests + run: npm run test:e2e + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + + build: + runs-on: ubuntu-latest + needs: [lint-and-test, e2e] + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: build + path: dist/ +``` + +--- + +## Pre-Commit Hooks + +Run checks locally before pushing. + +### Husky Setup (Node.js) + +```bash +# Install +npm install husky lint-staged --save-dev +npx husky init +``` + +```json +// package.json +{ + "scripts": { + "prepare": "husky" + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md}": [ + "prettier --write" + ] + } +} +``` + +```bash +# .husky/pre-commit +npm run lint-staged +npm test +``` + +### Pre-commit Framework (Python/Multi-language) + +```yaml +# .pre-commit-config.yaml +repos: + # Go + - repo: local + hooks: + - id: go-fmt + name: Go Format + entry: go fmt ./... + language: system + types: [go] + pass_filenames: false + + - id: go-vet + name: Go Vet + entry: go vet ./... + language: system + types: [go] + pass_filenames: false + + - id: go-test + name: Go Test + entry: go test ./... + language: system + types: [go] + pass_filenames: false + + # General + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-merge-conflict + + # Secrets + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets +``` + +```bash +# Install +pip install pre-commit +pre-commit install + +# Run manually +pre-commit run --all-files +``` + +--- + +## Testing in CI + +### Running Tests with Services + +```yaml +# PostgreSQL service +services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + +# Redis service + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 +``` + +### E2E Tests with Cypress + +```yaml +e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm ci + + - name: Start application + run: npm run start & + env: + DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} + + - name: Wait for server + run: npx wait-on http://localhost:3000 + + - name: Run Cypress + uses: cypress-io/github-action@v6 + with: + wait-on: http://localhost:3000 + + - name: Upload screenshots on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cypress-screenshots + path: cypress/screenshots +``` + +### Test Containers + +```go +// For integration tests with real databases +import ( + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func setupTestDB(t *testing.T) *sql.DB { + ctx := context.Background() + + container, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:16"), + postgres.WithDatabase("testdb"), + postgres.WithUsername("test"), + postgres.WithPassword("test"), + ) + require.NoError(t, err) + + t.Cleanup(func() { container.Terminate(ctx) }) + + connStr, _ := container.ConnectionString(ctx, "sslmode=disable") + db, _ := sql.Open("postgres", connStr) + + return db +} +``` + +--- + +## Deployment Pipelines + +### Deploy to Staging on Push + +```yaml +# .github/workflows/deploy-staging.yml +name: Deploy Staging + +on: + push: + branches: [develop] + +jobs: + deploy: + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build -t myapp:${{ github.sha }} . + + - name: Push to registry + run: | + echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin + docker tag myapp:${{ github.sha }} registry.example.com/myapp:staging + docker push registry.example.com/myapp:staging + + - name: Deploy to staging + run: | + kubectl set image deployment/myapp myapp=registry.example.com/myapp:staging + env: + KUBECONFIG: ${{ secrets.STAGING_KUBECONFIG }} +``` + +### Deploy to Production on Release + +```yaml +# .github/workflows/deploy-production.yml +name: Deploy Production + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build -t myapp:${{ github.event.release.tag_name }} . + + - name: Push to registry + run: | + docker tag myapp:${{ github.event.release.tag_name }} registry.example.com/myapp:${{ github.event.release.tag_name }} + docker tag myapp:${{ github.event.release.tag_name }} registry.example.com/myapp:latest + docker push registry.example.com/myapp:${{ github.event.release.tag_name }} + docker push registry.example.com/myapp:latest + + - name: Deploy to production + run: | + kubectl set image deployment/myapp myapp=registry.example.com/myapp:${{ github.event.release.tag_name }} + env: + KUBECONFIG: ${{ secrets.PROD_KUBECONFIG }} + + - name: Verify deployment + run: | + kubectl rollout status deployment/myapp --timeout=5m +``` + +### Database Migrations in CI + +```yaml +migrate: + runs-on: ubuntu-latest + needs: [test] + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Run migrations + run: | + migrate -path ./migrations -database ${{ secrets.DATABASE_URL }} up + env: + DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }} +``` + +--- + +## Rollback Strategies + +### Kubernetes Rollback + +```yaml +# In deployment workflow +- name: Deploy + id: deploy + run: | + kubectl set image deployment/myapp myapp=$IMAGE + kubectl rollout status deployment/myapp --timeout=5m + continue-on-error: true + +- name: Rollback on failure + if: steps.deploy.outcome == 'failure' + run: | + kubectl rollout undo deployment/myapp + echo "::error::Deployment failed, rolled back to previous version" + exit 1 +``` + +### Blue-Green Deployment + +```yaml +# Deploy to green environment +- name: Deploy to green + run: | + kubectl apply -f k8s/deployment-green.yaml + kubectl rollout status deployment/myapp-green + +# Health check +- name: Health check green + run: | + curl -f http://myapp-green.internal/health + +# Switch traffic +- name: Switch traffic to green + run: | + kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}' + +# Cleanup old deployment +- name: Remove blue + run: | + kubectl delete deployment myapp-blue +``` + +--- + +## Environment Management + +### GitHub Environments + +```yaml +jobs: + deploy-staging: + environment: staging # Requires approval if configured + env: + API_URL: ${{ vars.API_URL }} # Environment-specific variable + API_KEY: ${{ secrets.API_KEY }} # Environment-specific secret +``` + +### Environment-Specific Configs + +```yaml +# config/staging.yaml +database: + host: staging-db.example.com + pool_size: 5 + +# config/production.yaml +database: + host: prod-db.example.com + pool_size: 25 +``` + +```go +func LoadConfig() *Config { + env := os.Getenv("ENV") // staging, production + configFile := fmt.Sprintf("config/%s.yaml", env) + // Load config... +} +``` + +--- + +## Example Complete Workflows + +### Monorepo with Multiple Services + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + api: ${{ steps.filter.outputs.api }} + web: ${{ steps.filter.outputs.web }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + api: + - 'services/api/**' + web: + - 'services/web/**' + + test-api: + needs: detect-changes + if: needs.detect-changes.outputs.api == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/api + steps: + - uses: actions/checkout@v4 + - run: go test ./... + + test-web: + needs: detect-changes + if: needs.detect-changes.outputs.web == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/web + steps: + - uses: actions/checkout@v4 + - run: npm ci && npm test +``` + +--- + +## CI/CD Checklist + +### Pre-Merge Checks +- [ ] All tests pass (unit + E2E) +- [ ] Linting passes +- [ ] Type checking passes +- [ ] Coverage meets threshold +- [ ] Security scan clean +- [ ] Build succeeds + +### Deployment Checks +- [ ] Migrations run successfully +- [ ] Health checks pass +- [ ] Smoke tests pass +- [ ] Monitoring alerts clear +- [ ] Rollback tested + +### Secrets Management +- [ ] Secrets in GitHub Secrets +- [ ] No secrets in code +- [ ] Secrets rotated regularly +- [ ] Minimal access scope + +--- + +**Prev:** [Performance Optimization](./16-performance.md) | **Next:** [Advanced Topics](./18-advanced-topics.md) diff --git a/docs/18-advanced-topics.md b/docs/18-advanced-topics.md new file mode 100644 index 0000000..be06473 --- /dev/null +++ b/docs/18-advanced-topics.md @@ -0,0 +1,571 @@ +# Advanced Topics + +Advanced techniques for experienced Claude Code users. + +## MCP (Model Context Protocol) Servers + +MCP servers extend Claude Code's capabilities with custom tools. + +### What Are MCP Servers? + +MCP servers provide: +- Custom tools for specific domains (databases, APIs, services) +- Access to local resources (files, processes) +- Integration with external services +- Specialized functionality beyond built-in tools + +### Common MCP Servers + +| Server | Purpose | Use Case | +|--------|---------|----------| +| filesystem | File operations | Read/write files outside workspace | +| postgres | Database queries | Direct database access | +| github | GitHub API | PR management, issues | +| slack | Messaging | Team notifications | +| puppeteer | Browser automation | Web scraping, testing | + +### Setting Up MCP Servers + +**Configuration file:** +```json +// ~/.claude/mcp_servers.json +{ + "servers": { + "postgres": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-postgres"], + "env": { + "POSTGRES_URL": "postgres://user:pass@localhost:5432/db" + } + }, + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem"], + "env": { + "ALLOWED_PATHS": "/home/user/projects,/tmp" + } + } + } +} +``` + +### Creating Custom MCP Servers + +**Basic structure (TypeScript):** +```typescript +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +const server = new Server({ + name: "my-custom-server", + version: "1.0.0" +}, { + capabilities: { + tools: {} + } +}); + +// Define a tool +server.setRequestHandler("tools/list", async () => ({ + tools: [{ + name: "my_tool", + description: "Does something useful", + inputSchema: { + type: "object", + properties: { + param: { type: "string", description: "Input parameter" } + }, + required: ["param"] + } + }] +})); + +// Handle tool calls +server.setRequestHandler("tools/call", async (request) => { + if (request.params.name === "my_tool") { + const result = await doSomething(request.params.arguments.param); + return { content: [{ type: "text", text: result }] }; + } +}); + +// Start server +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +**Using your custom server:** +``` +// Tell Claude to use your MCP tool +Use the my_tool to process "input data" +``` + +--- + +## Multi-Repository Projects + +Managing multiple repositories with Claude Code. + +### Workspace Setup + +**VS Code multi-root workspace:** +```json +// project.code-workspace +{ + "folders": [ + { "path": "service-api" }, + { "path": "service-web" }, + { "path": "service-worker" }, + { "path": "infrastructure" } + ], + "settings": { + "files.exclude": { + "**/node_modules": true + } + } +} +``` + +### Cross-Repo Context + +**Share context with Claude:** +``` +I'm working on a microservices project with multiple repos: + +1. service-api/ - Go REST API + - CLAUDE.md defines API conventions + - Endpoints in internal/handlers/ + +2. service-web/ - React frontend + - CLAUDE.md defines component patterns + - API client in src/api/ + +3. infrastructure/ - Docker, Kubernetes + - CLAUDE.md defines deployment patterns + - Shared types in proto/ + +When making changes, ensure consistency across repos. +Current task: Add a new "orders" endpoint... +``` + +### Shared Types Between Services + +**Protocol Buffers for shared types:** +```protobuf +// proto/order.proto +syntax = "proto3"; + +message Order { + int64 id = 1; + int64 user_id = 2; + repeated OrderItem items = 3; + string status = 4; + double total = 5; +} + +message OrderItem { + int64 product_id = 1; + int32 quantity = 2; + double price = 3; +} +``` + +**Generate for each language:** +```bash +# Go +protoc --go_out=service-api/pkg/proto order.proto + +# TypeScript +protoc --ts_out=service-web/src/proto order.proto +``` + +### Coordinated Changes + +**Multi-repo PR workflow:** +``` +Making a change that affects multiple repos: + +1. API changes (service-api) + - New endpoint: POST /api/orders + - PR: api-repo#123 + +2. Frontend changes (service-web) + - New OrderForm component + - API client update + - PR: web-repo#456 + +3. Infrastructure changes (infrastructure) + - Update API deployment + - PR: infra-repo#789 + +Merge order: API → Infrastructure → Frontend +``` + +--- + +## Advanced CLAUDE.md Patterns + +### Context Optimization + +**Structured sections for large projects:** +```markdown +# CLAUDE.md + +## Quick Reference +[Most frequently needed info - always read this] + +## Architecture Overview +[High-level system design] + +## Current Sprint Context +[What we're working on now - update weekly] + +## Deep Dive Sections +### Database Schema +[Link to separate file: docs/database.md] + +### API Reference +[Link to separate file: docs/api.md] + +## Historical Decisions +[Link to ADR folder: docs/adr/] +``` + +### Project-Specific Commands + +**Define common workflows:** +```markdown +## Common Prompts + +### Adding a New API Endpoint +``` +Add a new endpoint for [resource]: +1. Create migration in migrations/ +2. Add repository methods in internal/repository/ +3. Create handler in internal/handlers/ +4. Add routes in cmd/server/routes.go +5. Write E2E tests in tests/e2e/ +6. Update API documentation + +Follow existing patterns from the users endpoint. +``` + +### Debugging a Test Failure +``` +Debug the failing test: +1. Read the test file +2. Identify the assertion that fails +3. Check the handler/repository being tested +4. Add debug logging if needed +5. Fix the issue +6. Verify all tests pass +``` +``` + +### Dynamic Context + +**Include current state:** +```markdown +## Current Development State + +**Last Updated:** 2025-01-15 + +### In Progress +- Feature: Order management (branch: feature/orders) +- Blocked: Waiting for payment gateway access + +### Recently Completed +- User authentication (merged: 2025-01-10) +- Product catalog (merged: 2025-01-12) + +### Known Issues +- Performance issue on /api/products with large datasets (#123) +- Flaky test in auth_test.go (#124) +``` + +--- + +## Complex Refactoring + +### Large-Scale Codebase Changes + +**Incremental migration strategy:** +``` +We need to migrate from REST to GraphQL. Plan: + +Phase 1: Add GraphQL alongside REST +- Set up GraphQL server +- Create schemas for existing models +- Add resolvers that call existing services +- Both REST and GraphQL work + +Phase 2: Migrate high-traffic endpoints +- Orders API → GraphQL +- Products API → GraphQL +- Update frontend to use GraphQL for these + +Phase 3: Migrate remaining endpoints +- Users API → GraphQL +- Admin API → GraphQL + +Phase 4: Deprecate REST +- Add deprecation warnings +- Set sunset date +- Remove REST endpoints + +Each phase is a separate PR. Tests must pass at each phase. +``` + +### Maintaining Tests During Refactoring + +``` +Refactoring the repository layer: + +Rules: +1. Tests must pass before AND after each change +2. Run tests after every file change +3. If tests fail, fix before continuing +4. Don't change tests and implementation in same commit + +Process: +1. Create new implementation alongside old +2. Add tests for new implementation +3. Migrate callers one at a time +4. Remove old implementation when unused +5. Clean up + +Current step: [specify which step] +``` + +### Safe Renaming + +``` +Rename UserService to AccountService across codebase: + +Steps: +1. Create AccountService as alias to UserService +2. Update all imports to use AccountService +3. Run tests - must pass +4. Update internal implementation +5. Remove UserService alias +6. Update documentation + +Do NOT: +- Rename and update usages in same commit +- Skip test runs between steps +- Update tests before implementation +``` + +--- + +## Working with Legacy Code + +### Understanding Undocumented Code + +``` +I need to understand this legacy code: + +File: src/legacy/processor.go + +Please: +1. Read the file and all related files it imports +2. Trace the data flow from input to output +3. Identify side effects (database, external APIs) +4. Document assumptions and magic numbers +5. Create a summary of what this code does + +Don't modify anything yet - just document. +``` + +### Adding Tests to Legacy Code + +``` +Add tests to legacy code without modifying it: + +File: src/legacy/calculator.go + +Approach: +1. Read and understand the code +2. Identify public functions to test +3. Create test file: calculator_test.go +4. Write tests for current behavior (not ideal behavior) +5. Tests should pass with existing code +6. Document any bugs found (don't fix yet) + +Goal: Establish a safety net before refactoring. +``` + +### Incremental Modernization + +``` +Modernize legacy authentication module: + +Current state: +- Uses deprecated crypto library +- No tests +- Mixed concerns (auth + session + user) + +Target state: +- Modern crypto (bcrypt) +- 80% test coverage +- Separated concerns + +Plan: +Phase 1: Add tests for current behavior +Phase 2: Extract session management +Phase 3: Extract user management +Phase 4: Update crypto library +Phase 5: Add integration tests + +We're on Phase 1. Add tests without changing code. +``` + +--- + +## Context Window Optimization + +### Managing Large Codebases + +**Strategies for staying within context limits:** + +1. **Reference, don't repeat:** +``` +See the pattern in src/handlers/users.go lines 50-80. +Apply the same pattern to the new orders handler. +``` + +2. **Summarize large files:** +``` +The database schema has 30 tables. Relevant ones for this task: +- users (id, email, password_hash) +- orders (id, user_id, total, status) +- order_items (id, order_id, product_id, quantity) +``` + +3. **Focus on diff:** +``` +Only these files need changes: +1. src/handlers/orders.go - Add CreateOrder handler +2. src/repository/orders.go - Add Create method +3. tests/orders_test.go - Add tests + +Other files are stable - don't read unless needed. +``` + +### Efficient File Reading + +``` +Read only what's needed: + +For this task (adding order validation): +1. Read: src/handlers/orders.go (need to see current handler) +2. Read: src/validation/rules.go (need existing patterns) +3. Skip: src/repository/* (not changing data layer) +4. Skip: tests/* (will update after implementation) + +Start with handlers/orders.go +``` + +--- + +## Extending Claude Code + +### Custom Slash Commands + +Create project-specific commands: + +```markdown + +# New Endpoint Command + +Create a new API endpoint with: +- Handler in internal/handlers/ +- Repository method in internal/repository/ +- Migration if needed +- E2E tests in tests/e2e/ +- Unit tests for business logic + +Arguments: +- $RESOURCE: The resource name (e.g., "orders") +- $METHODS: HTTP methods to support (e.g., "GET,POST,PUT,DELETE") + +Follow patterns from existing endpoints. +Start with the database migration. +``` + +**Usage:** +``` +/project:new-endpoint orders GET,POST,PUT,DELETE +``` + +### Automation Scripts + +**Git hooks integration:** +```bash +#!/bin/bash +# .git/hooks/prepare-commit-msg + +# Auto-generate commit message suggestion +if [ -z "$2" ]; then + # Get changed files + FILES=$(git diff --cached --name-only) + + # Create prompt for Claude + echo "Based on these changed files, suggest a commit message:" > /tmp/commit-prompt + echo "$FILES" >> /tmp/commit-prompt + git diff --cached >> /tmp/commit-prompt + + # Could integrate with Claude API here + # For now, just remind the developer + echo "# Changed files:" >> "$1" + echo "$FILES" | sed 's/^/# /' >> "$1" +fi +``` + +### Integration with Other Tools + +**VS Code tasks:** +```json +// .vscode/tasks.json +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Tests for Current File", + "type": "shell", + "command": "go test -v ./${relativeFileDirname}/...", + "group": "test", + "presentation": { + "reveal": "always" + } + }, + { + "label": "Generate Mocks", + "type": "shell", + "command": "mockgen -source=${relativeFile} -destination=mocks/${fileBasenameNoExtension}_mock.go", + "group": "build" + } + ] +} +``` + +--- + +## Summary + +**Key Advanced Techniques:** +- MCP servers extend Claude's capabilities +- Multi-repo projects need coordinated context +- CLAUDE.md can include dynamic state +- Refactoring requires incremental, tested steps +- Legacy code needs tests before changes +- Optimize context usage for large codebases + +**When to Use Advanced Techniques:** +- Project complexity exceeds basic patterns +- Standard approaches don't fit your needs +- Performance or scale requires optimization +- Legacy systems need careful handling + +--- + +**Prev:** [CI/CD and Deployment](./17-ci-cd.md) | **Next:** [Troubleshooting](./19-troubleshooting.md) diff --git a/examples/my-api-project/CLAUDE.md b/examples/my-api-project/CLAUDE.md new file mode 100644 index 0000000..e013fbf --- /dev/null +++ b/examples/my-api-project/CLAUDE.md @@ -0,0 +1,277 @@ +# CLAUDE.md - my-api-project + +## Overview + +Go REST API with JWT authentication and PostgreSQL database. This is a reference implementation for the Claude Code workshop. + +## Architecture + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Client │────▶│ API │────▶│ Database │ +│ │ │ (Go) │ │ (PostgreSQL)│ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ┌──────┴──────┐ + │ │ + ┌─────▼─────┐ ┌─────▼─────┐ + │ Handlers │ │ Middleware│ + └─────┬─────┘ └───────────┘ + │ + ┌─────▼─────┐ + │Repository │ + └─────┬─────┘ + │ + ┌─────▼─────┐ + │ Models │ + └───────────┘ +``` + +## Tech Stack + +- **Language:** Go 1.21+ +- **Framework:** gorilla/mux (routing) +- **Database:** PostgreSQL 16 +- **Authentication:** JWT (golang-jwt/jwt) +- **Password Hashing:** bcrypt +- **Container:** Docker + Docker Compose + +## Project Structure + +``` +my-api-project/ +├── cmd/ +│ └── server/ +│ └── main.go # Application entry point +├── internal/ +│ ├── handlers/ # HTTP handlers +│ │ ├── auth.go # Auth handlers (register, login, refresh) +│ │ └── user.go # User handlers +│ ├── middleware/ # HTTP middleware +│ │ └── auth.go # JWT authentication middleware +│ ├── models/ # Data models +│ │ └── user.go # User model +│ └── repository/ # Database operations +│ └── user.go # User repository +├── migrations/ # Database migrations +│ ├── 001_create_users.up.sql +│ └── 001_create_users.down.sql +├── tests/ # Test files +│ ├── e2e/ # E2E tests +│ └── unit/ # Unit tests +├── docker-compose.yml +├── Dockerfile +├── go.mod +├── go.sum +└── CLAUDE.md +``` + +## API Endpoints + +### Authentication + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| POST | `/api/auth/register` | Register new user | No | +| POST | `/api/auth/login` | Login, returns JWT | No | +| POST | `/api/auth/refresh` | Refresh JWT token | Yes | +| POST | `/api/auth/logout` | Invalidate token | Yes | + +### Users + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| GET | `/api/users/me` | Get current user | Yes | +| PUT | `/api/users/me` | Update current user | Yes | + +### Request/Response Format + +**Register Request:** +```json +{ + "email": "user@example.com", + "password": "securepassword123" +} +``` + +**Login Response:** +```json +{ + "token": "eyJhbGciOiJIUzI1NiIs...", + "expires_at": "2025-01-16T10:00:00Z" +} +``` + +**Error Response:** +```json +{ + "error": "invalid credentials", + "code": "AUTH_INVALID_CREDENTIALS" +} +``` + +## Git Workflow + +### Branch Naming +``` +feature/ - New features (feature/user-auth) +fix/ - Bug fixes (fix/login-validation) +refactor/ - Code refactoring (refactor/repository-layer) +test/ - Test additions (test/auth-e2e) +docs/ - Documentation (docs/api-readme) +``` + +### Commit Messages + +Use conventional commits: +``` +feat(auth): add JWT token refresh endpoint +fix(user): validate email format on registration +test(auth): add E2E tests for login flow +docs(api): update endpoint documentation +``` + +### Pre-Commit Checklist + +Before every commit, ensure: +- [ ] `go fmt ./...` - Code is formatted +- [ ] `go vet ./...` - No suspicious constructs +- [ ] `go test ./...` - All tests pass +- [ ] `go build ./...` - Code compiles +- [ ] Coverage meets minimum threshold (70%) + +## Testing Strategy + +### E2E Tests (Mandatory) +Test complete user journeys: +- Registration flow +- Login flow +- Protected endpoint access +- Token refresh +- Error scenarios + +### Unit Tests (Mandatory) +Test business logic: +- Password hashing/verification +- JWT generation/validation +- Email validation +- Repository methods (with mocks) + +### Coverage Targets +- Overall: 70% minimum +- Handlers: 80% minimum +- Repository: 70% minimum + +### Running Tests +```bash +# All tests +go test ./... + +# With coverage +go test -cover ./... + +# E2E tests only +go test ./tests/e2e/... + +# Unit tests only +go test ./tests/unit/... +``` + +## Environment Variables + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `DATABASE_URL` | PostgreSQL connection string | Yes | - | +| `JWT_SECRET` | Secret for JWT signing | Yes | - | +| `JWT_EXPIRY` | Token expiry duration | No | 24h | +| `PORT` | Server port | No | 8080 | +| `ENV` | Environment (dev/prod) | No | dev | + +### Example .env +```bash +DATABASE_URL=postgres://user:pass@localhost:5432/myapi?sslmode=disable +JWT_SECRET=your-256-bit-secret-key-here +JWT_EXPIRY=24h +PORT=8080 +ENV=dev +``` + +## Common Commands + +```bash +# Development +go run cmd/server/main.go # Start server +go build -o bin/server ./cmd/server # Build binary + +# Database +psql -d myapi -f migrations/001_create_users.up.sql # Run migration +psql -d myapi -f migrations/001_create_users.down.sql # Rollback + +# Docker +docker-compose up -d # Start all services +docker-compose down # Stop all services +docker-compose logs -f api # View API logs + +# Testing +go test ./... -v # Verbose test output +go test -coverprofile=coverage.out ./... # Generate coverage +go tool cover -html=coverage.out # View coverage report +``` + +## Code Patterns + +### Handler Pattern +```go +func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { + var req RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "invalid request body") + return + } + + // Validate + if err := validateEmail(req.Email); err != nil { + respondError(w, http.StatusBadRequest, err.Error()) + return + } + + // Process + user, err := h.userRepo.Create(r.Context(), req.Email, req.Password) + if err != nil { + respondError(w, http.StatusInternalServerError, "failed to create user") + return + } + + respondJSON(w, http.StatusCreated, user) +} +``` + +### Repository Pattern +```go +type UserRepository interface { + Create(ctx context.Context, email, password string) (*User, error) + GetByEmail(ctx context.Context, email string) (*User, error) + GetByID(ctx context.Context, id int) (*User, error) + Update(ctx context.Context, user *User) error +} +``` + +## Security Considerations + +- Passwords hashed with bcrypt (cost factor 12) +- JWT tokens expire after 24 hours +- Rate limiting on auth endpoints (10 requests/minute) +- Input validation on all endpoints +- SQL injection prevented via parameterized queries +- CORS configured for allowed origins only + +## Development Workflow + +1. **Read the spec** - Understand what you're building +2. **Write migration** - Database schema first +3. **Implement repository** - Data access layer +4. **Build handlers** - HTTP endpoints +5. **Add middleware** - Authentication, logging +6. **Write tests** - E2E then unit +7. **Run pre-commit checks** - Format, lint, test +8. **Commit and push** - Small, focused commits diff --git a/examples/my-api-project/README.md b/examples/my-api-project/README.md new file mode 100644 index 0000000..b19877e --- /dev/null +++ b/examples/my-api-project/README.md @@ -0,0 +1,74 @@ +# my-api-project Reference Example + +This is a reference implementation for the Claude Code workshop exercises (Pages 9-19). + +## Purpose + +This example shows what a completed Go API project looks like after following the workshop exercises. Use it to: + +- **Compare** your generated CLAUDE.md against a production-ready version +- **Reference** proper migration structure with up/down files +- **Understand** trigger patterns for automatic timestamp updates + +> **Important:** This is a reference for comparison, not copy-paste. Complete the exercises yourself first, then compare your output. + +## What's Included + +``` +my-api-project/ +├── README.md # This file +├── CLAUDE.md # Complete project configuration +└── migrations/ + ├── 001_create_users.up.sql # User table creation + └── 001_create_users.down.sql # Rollback migration +``` + +## How to Use + +### During Workshop + +1. Complete each exercise independently +2. After finishing, compare your output to these files +3. Note differences - different approaches are OK if they work! + +### After Workshop + +- Use as a template for starting new Go API projects +- Reference the patterns used (indexes, triggers, conventions) +- Adapt the CLAUDE.md structure for your own projects + +## Key Patterns Demonstrated + +### CLAUDE.md + +- Project overview and architecture +- Tech stack with specific versions (Go 1.21+, PostgreSQL 16) +- Git workflow conventions matching workshop teachings +- Testing strategy with coverage targets +- Environment configuration +- Pre-commit checklist + +### Database Migration + +- Serial primary key pattern +- Email uniqueness constraint with index +- Automatic `updated_at` trigger +- Proper up/down migration pair for rollback support +- Index on frequently-queried columns + +## Workshop Exercise Reference + +| Workshop Page | This Example | +|---------------|--------------| +| Page 9: Create CLAUDE.md | See `CLAUDE.md` | +| Page 14-15: Database Schema | See `migrations/001_create_users.up.sql` | + +## Note + +This is a minimal example focused on the workshop exercises. For a comprehensive CLAUDE.md template covering all aspects of a production project, see [`../claude-md-template.md`](../claude-md-template.md). + +## Related Documentation + +- [Documentation Organization](../../docs/12-documentation-organization.md) - How to structure project documentation +- [Git Workflow](../../docs/11-git-workflow.md) - Branch and commit conventions +- [Testing Strategy](../../docs/06-testing-strategy.md) - E2E and unit testing approach diff --git a/examples/my-api-project/migrations/001_create_users.down.sql b/examples/my-api-project/migrations/001_create_users.down.sql new file mode 100644 index 0000000..14ca04f --- /dev/null +++ b/examples/my-api-project/migrations/001_create_users.down.sql @@ -0,0 +1,15 @@ +-- Migration: Rollback users table +-- Version: 001 +-- Description: Remove user table and related objects + +-- Drop trigger first (depends on function) +DROP TRIGGER IF EXISTS update_users_updated_at ON users; + +-- Drop function +DROP FUNCTION IF EXISTS update_updated_at_column(); + +-- Drop index +DROP INDEX IF EXISTS idx_users_email; + +-- Drop table +DROP TABLE IF EXISTS users; diff --git a/examples/my-api-project/migrations/001_create_users.up.sql b/examples/my-api-project/migrations/001_create_users.up.sql new file mode 100644 index 0000000..4c1bd67 --- /dev/null +++ b/examples/my-api-project/migrations/001_create_users.up.sql @@ -0,0 +1,35 @@ +-- Migration: Create users table +-- Version: 001 +-- Description: Initial user table for authentication + +-- Create users table +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Create index on email for faster lookups during login +CREATE INDEX idx_users_email ON users(email); + +-- Create function to automatically update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create trigger to call the function before any update +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- Add comment for documentation +COMMENT ON TABLE users IS 'User accounts for authentication'; +COMMENT ON COLUMN users.email IS 'Unique email address for login'; +COMMENT ON COLUMN users.password_hash IS 'bcrypt hashed password'; diff --git a/prompts.md b/prompts.md index 4d5e049..a973a95 100644 --- a/prompts.md +++ b/prompts.md @@ -4,7 +4,7 @@ Copy and paste these prompts during the workshop exercises. --- -## Page 9: Create Your First Project +## Page 23: Create Your First Project **PROMPT:** ``` @@ -27,7 +27,7 @@ Claude creates a comprehensive CLAUDE.md with architecture, commands, and conven --- -## Page 10: Initialize Git Workflow +## Page 24: Initialize Git Workflow **PROMPT:** ``` @@ -44,7 +44,7 @@ Git repository with proper branch structure and commit conventions --- -## Page 13: Step 1 - Write Specification +## Page 27: Step 1 - Write Specification **PROMPT:** ``` @@ -74,7 +74,7 @@ Claude asks clarifying questions and confirms the specification --- -## Page 15: Step 2 - Review Schema +## Page 29: Step 2 - Review Schema **PROMPT:** ``` @@ -97,7 +97,7 @@ Database table created successfully with proper indexes --- -## Page 17: Step 4 - Request API Implementation +## Page 31: Step 4 - Request API Implementation **PROMPT:** ``` @@ -127,7 +127,7 @@ Claude creates handler files with validation and error handling --- -## Page 19: Step 5 - Request API Tests +## Page 33: Step 5 - Request API Tests **PROMPT:** ``` @@ -152,7 +152,7 @@ Cypress test file created with all test cases --- -## Page 21: Run API Tests +## Page 35: Run API Tests **PROMPT:** ``` @@ -171,7 +171,7 @@ All API tests pass (green checkmarks in terminal) --- -## Page 22: Step 6 - Request Frontend +## Page 36: Step 6 - Request Frontend **PROMPT:** ``` @@ -198,7 +198,7 @@ React components created with forms and state management --- -## Page 24: Step 7 - Request UI Tests +## Page 38: Step 7 - Request UI Tests **PROMPT:** ``` @@ -220,7 +220,7 @@ Cypress UI test file created with user journey tests --- -## Page 28: Practice: Write E2E Test +## Page 42: Practice: Write E2E Test **PROMPT:** ``` @@ -242,7 +242,7 @@ Complete password reset feature with passing E2E tests --- -## Page 32: Practice: Proper Git Workflow +## Page 46: Practice: Proper Git Workflow **PROMPT:** ``` @@ -267,7 +267,7 @@ PR created with passing CI checks and proper commit messages --- -## Page 34: Practice: Better Prompts +## Page 48: Practice: Better Prompts **PROMPT:** ``` @@ -296,7 +296,7 @@ You create a comprehensive, specific prompt with clear requirements --- -## Page 38: Final Exercise: Complete Feature +## Page 52: Final Exercise: Complete Feature **PROMPT:** ``` @@ -328,7 +328,7 @@ Complete profile feature with passing tests and PR ready for review --- -## Page 44: EXERCISE: Multi-Phase Task Manager (Phase 0) +## Page 58: EXERCISE: Multi-Phase Task Manager (Phase 0) **PROMPT:** ``` @@ -371,7 +371,7 @@ devplan.md created with 4 phases and dependency mapping --- -## Page 45: EXERCISE: Create Progress Tracker +## Page 59: EXERCISE: Create Progress Tracker **PROMPT:** ``` @@ -406,7 +406,7 @@ devprogress.md and database.md created --- -## Page 46: EXERCISE: Implement Phase 0 Database +## Page 60: EXERCISE: Implement Phase 0 Database **PROMPT:** ``` @@ -444,7 +444,7 @@ Migrations created and progress updated --- -## Page 47: EXERCISE: Implement Auth System +## Page 61: EXERCISE: Implement Auth System **PROMPT:** ``` @@ -478,3 +478,58 @@ Auth implemented and progress updated --- +## NEW: Documentation Organization Exercise + +**PROMPT:** +``` +I'm starting a new project. Help me decide on documentation structure. + +Project details: +- Solo developer +- 3-month project +- Single Go API service +- ~40 tasks estimated + +Based on these factors, should I use: +A) Simple flat structure (plan.md, architecture.md at root) +B) Nested structure (project/planning/, project/specs/) + +Create the appropriate documentation files for my choice. +Include: +- plan.md or devplan.md with milestones +- architecture.md with system design +- requirements.md with key features +``` + +**EXPECTED RESULT:** +Claude recommends nested structure (40 tasks, 3 months) and creates starter files + +**REFERENCE:** +See [docs/12-documentation-organization.md](docs/12-documentation-organization.md) for decision criteria and templates. + +--- + +## NEW: Migrate Flat to Nested Structure + +**PROMPT:** +``` +My project has grown. I started with simple flat docs: +- plan.md +- architecture.md +- requirements.md + +Now I have 50+ tasks across 5 features. Help me migrate to +the nested structure: +- Move plan.md content to project/planning/devplan.md +- Create project/planning/devprogress.md for tracking +- Split requirements into project/specs/ by feature +- Keep architecture.md updated + +Preserve all existing content during migration. +``` + +**EXPECTED RESULT:** +Documentation migrated to nested structure with content preserved + +--- + From e5aa69a40734617dddd9d0073ad20ec038123170 Mon Sep 17 00:00:00 2001 From: Emmanuel Andre Date: Thu, 4 Dec 2025 19:56:56 +0800 Subject: [PATCH 2/4] fix: resolve slide overflow and add tool-agnostic support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix content overflow on slides 6, 16, 22, 30, 32, 37, 41, 46, 59, 62, 83, 85 - Make slides tool-agnostic to support Claude Code and Windsurf IDE - Add data privacy slide explaining how code is handled - Replace CLAUDE.md references with "CLAUDE.md / Windsurf Rules" - Condense slide content to prevent spill-over 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- claude-code-interactive-tutorial.pdf | Bin 88186 -> 85336 bytes create_interactive_presentation_v2.py | 261 ++++++++++++-------------- prompts.md | 34 ++-- 3 files changed, 136 insertions(+), 159 deletions(-) diff --git a/claude-code-interactive-tutorial.pdf b/claude-code-interactive-tutorial.pdf index ae187d16ba010a75cae955a2bf01970906ca24da..ab23e0b9dd37f10ffea2dd9f1bae8c8630ee958b 100644 GIT binary patch delta 28904 zcmZU)d+_U8R^M6o9=ZvA`}XVhz2`K|qxJGbwq(h&Wy`W9KV`|X{F3~TU$QM*l3%hV z+f_-bXir0v2T7o&T}9Ubfg*IOYbsTlz@df=Nq8hfhAAeMB11w51QMvBhDl{AzyQ;b z38Ql^zpjD#{q_5JZF$RETYIndU2Fg0-}}J7{+o`CUeCWN$U;4WD zd;gw?@}c+t`?kkl`|yj$yKlYU>f0W@w_iMd<*W3mkGy#N(1)LF|1O~2+fVf6kG^=2 zA9*78V=o^6_g5+X@fVMu{OFVIKL_-eKK4Zat4}5{`~QOeU*~f^T$7kJ=wkz(6`2)=%?`K4+(psznpmf_!#~~E#mp(7ypF5_woIS zr_X`8`HEgWjV<-!@w?=!mam9Ny?Fe1@`?T+pwCfH^b_e94>9#b z-%h`Hd^G(;&CH9(zxpb@lYQ~{oAi_I-vqSIJkbv_&mU;^sdp#${PEkb>D3!fu+Ja= z1M{loEBb?g{;S**{Y3uxqt3oh-~0GT{^|23_x$lMze?}$&mZ4cc(VPQfK2X*ey|wy z{Cf3x_3cjSy~lszU){$mN|fJw{8;hnu70ol;&Cs%y6)AO{KLwNhfsc^Z>zp|yj^*s zM(xGp7oO-%CiVEiIucy=Kh?ka@k_#oAHP#ieodODvX6*ZfB&D0_+6i*KSO-xwAy_r zpZ`#za=N+K&tx+%-;?fdK7L64`o~Xwmc6Ia%!fGcLzk<+Ssp)BU;0Bj*BJNlP&pOP z;`J?2FP@IsbzxoWfg9&=iVD+yaeS6nl znD{|R<#U`9G|9U1Gm!C$pb zrS?}{8^!FML4)N%a$-|h4(U6UbD3ZVF;cv#X!D*?ldIP3P%OquiY?`;Fv7`-F=s~8 z64!J6S+Mk}XJM2T_rk^PA3C8>tYxtkHF(RoaLMK}Y6-p4cXK zOOcJa#H_(&s2gY~czIvH0SmBIuj%BlRl8(T?M)`lmDlFBtm(|W7iB`d#JNNOAsD>v z_nk+#|IV9C_VL5t#lHMV>wDh(NZ;Y7wXy_AGc3b(UzvkVuKUaE^qjkiyM9y3bn+SEPbULR}>0t3Iv+3*krKz$& z$1i14m-Frz1V2U2u16&vmVo-H!&d)iP9rY-zg;>CHZN zY1Cu%WOCd#EA1_KY9ixSf6po6cxSZ+`Op>tbM5|WW2@)!t-jr95zvq0w%G&7YyCIkAI=$A7AP*AKWRmB{+Qi zMh|?0e|%6)Vw*&pgxu`0x|Of!MgbjA*C9v}l(bWT=}bTk>P0=9Y>g zmTi8Q@5e5N8;9z7Dx*Y;iTr3eu0|bUW?Z9cu5cj?N0(ht@vP37^f`W32(q3{F)6 zoyU$xWRpq7hutaOX-3=zy_9B3)hz80fuW@8z^5z9Jk+hMh%wpkj;qk)C$vv}0Nh)3 z_3&bZPnT~~M{UTN-<3&VF=+=#3a!&NxEhUH|0 z=o1QYq(Z&1j~(4sG16;Cb2kq+Q}xiy-WF9mL869vHiXy9*fAp{Y-E0?Ot>NFB`I@M$s4le>8_ z70Xg%rrge~dyDR%B0H>8>2>NCkk`*=h$$^;wHBs`!r0PrVp@ZtN{R0;^WLd9NSOFG z(LU_=E5T3a>NAT%Hs6p-_G=wD5l``*WHVQccP0~M7%t6ib|eIY)Vb44@=p!Wjf`QcLy$)6duG7zK zOnzd(yJR}AZb`5>1eno+yt+Tkk~&D}jjz+Bw0qO4AO+a>2= zWAe1X^o8HgF3YZ}Iv8Xj;n^5Fm1?kLwZ>?lakSd0%d5Q$E=Bp_o@JA=k4DWMgSzsd zRa(r%;w&FgbGIdzvbYh9SV&_us^#j-!PSTi_s8_OrDfxKyED3zwP1^g9;;%o0$Li>me^z)7v zT5Wf3z2es*r^SLmtzbjIs#`5o0Kv8xivZOfeGu2xoCb$mH;*e#X0gK^X5bvr2=ZE-xZTkrrF*xo3m z?IIzw-!X2A&MAe-G31u^wMNg?7^AVTiq#g7J0L__-1osCqN`!hokZ&+u60H*RT6q zI{Wfl=JXpM9-K$(A-d>9`CQ?#9Cu%SY>|ETHXjC023Hu*Dmf$f`hxPDeK7VrFaN#? zKYP0j!j&Wm)aVo5oV_;xg2CQt>Q65p!P~))lT|A`ZrsqY0`HY%Ls%mVTWA>hPGr`j zH~Dt6NA{f@T&XUJ2D0C4%wXukGbMC#bGp#A<)*fj;D%U8=@+$ z^!bppEwMB|C)+phUc}Nz`YTO}CwJRFRM4kV<5 zi&)dv{erZ>JCSsLU&+@G+Zf?AZ_Q#WC)JF;KVur~F5g+kHO^U#T-_k-p`0!72g2T# zjz~Cy`mN@!XGvAZzjv1zzp7@WbaXc~hB4@>G+|E8iwT;@_jP3*8HeWNn!$5jDJo7f zuqku&_9|55Qi$7%Q-xlk@@6?&`w9RHNwbKT;E84RV?)+U5fyqfYZEP`7zqo*jK*y$ z;Zi0#Yn_RzIp*s<{$d||k zf)-c1R=7~m0jjmJj$7!YbEFHT+Iy&56!kGP4E)rxaz&9vdQJ7c%T~`5t4(CQpZc*; zgHedhXe0s3A}<+vG%?%wY>dQqr~tHWg@LZ6eVk`j5i924cR=Gqxjb%I6ik^9I5$Bqt*F-3bG_-UP2l(*+EK1Sgh9~sc5#eJjs0oqtfO>1 zA+_7*;jl%e)79C6j{@;nLc3^CoM{VDi4gmU+Cx)Bq3C2c*E^DFGui4Yg2`@Y+3-i_ zqaizy9wZpO1ac^zn*KQ*o+8ytR^%Feq={p}25=mnVl#_qxrL3=LQ34SR_*VmzC}dG zUVY0;D_GZtCH|Hd_2pPNB$Z6RTB;vvR@4=%t0sR&^Sw)cnUaKi#>Y_^gih;{F~Tt; zlG84i72ybWH)87JnO^3#xw)qhA&K^9UC|0Y@g*%%I*bUdl#cGJ@o=?Wxs;+!_gGQ7 zT|(t6mZV2cmH>}R`^Y2D>ER_O9B!j#;j zUu9C}K$*gDrUe%xTA{&7xely~oqVA-N1Rc;yr~XhJ_RLxp=iZtP-8UbTOgHNgd+ax zDyF*!ld7N7dZUvsqKT2a<~%D*=hNBIJVqRLZV*$+s=GNXvCncFcYV1&M*WXG{)@%l zGsK@=oZ~!{AmvvthK8S)A-J9~7K4%@%EOJ{OPe%4uWN(qu9Be;ry<+85Ij$j1?I(Z zl3c{peW6lVHqOlha-Cz_8?j~9ZV=mK-zbl&yOl&%9=zOOpvCB&LC#OM1 zlZMU}%cl0~#@`W{Chv2T93%rb(Ju&kNUh)#6WJW*^0VrqR57 zcDD^MHD;ZKX6le_PV3!KJ(Bj|O}52%G1NVs6@OHkF?3PwoerE>&XLu1Ow5!=;B^oW zJP};ufHJjd*BFcOhog9TF4UW~_9;^gx2SMBUaqC(GTd0B-o-bfA~6ut-kqGS@aw!6 zu^X=B#jpDuH5W=?ri`uarXk%rIDf#dGr3rt#GMrtX&E)Sri^QN6qczbUM<4=oxIYH zW;Xch=622$dc7ciB$}!!ji-D=8B_eIv{Q^}dlf;b!Ky3M44TN0JG)6|^-Q)*XY zZWm+`o%PdBX{EU==`ZTtHk8_(ti>)x<417B%!yF9VMd19uw5L63(7`{fJkDu7P9^X zvLf(2FZYJ6{4t}A+;l0$$UF7`UD?8NMukMUP2~Vxr(3<7a5Q5B%$^erP(zwZtq4^O zW>1vtw$BKiUi1tJt3?B(z*U0WOi<>W*6jEoW$JOI=*TeN5{B2@Sw38yO^QCH!i(10 z-_zi#*pQNse}8@V@r7OfPX7NUHFIc~JOZU?CA=}$OUevSz?EqExQ$lCB@!2ciG{HK zHhaC0HaloHZGRvPMv0LyRFK|iREj04xY=J0s+CESnzpmju6>GJmcucC37P)q{{V2C$#j|z{JcoK!8;$Djjp=QhbV!{E z_y|k}erA`W8t6LMX^`QX_oeYj|c((X*h^=f7f_G$I(wY-i#Jyt@4YPd8xs-~mQjBN6BHM%ouiWopm z&5go~1XQ}KkqAL8U~4;g{IDkQQ@BW9(3I`@Hwh9R%sv zWUG)GoQJh(Z92RIy=Z4c!eC`sF{|5L>0scNQ+8wMwU#-CE9`onMkOT{&PC#OUR|tv zx9eqaJOf~Oyh9Z-dtH_`<7l}h#zM&!GE78f-PyLqN13|KxVqg*qgJkpO{9(x85oJ4 zf%W87E=K{K*s&=i{(TqehDJ!KszQA~r<^2Xz#~nGj3t}NOZHS7Wpjsyr|lB_vKhEjt|?nbue01`=P;KG^YSutI0=w^6a8qKa0*mXg$5w~I!779zR zyfgb?;eeWl#ZCRxI8BY6BeCmC0)S>BRVm!LCEXY={F5E7p=D9)u8c~qaY#_Y0y&yU zr<~1};aFovA@suAucu=l%_p zRUyLO^jsq6W(M{~(Uz9cnz*W(iG!wc(e|x>7xGD=UM%cdNx4ula`W-2tFe4nlc^+m zQ7Y%W--hsFLBp2gvq9)VexN81ky)$GP8+cso7G$O1z{}A9(FsVR(jOf>jksg&vJOr z1FQD(6s)?|Qmm_{g1RHGY=pyREIQ8T-Slu7&D{^tiU4)C$8|3fX2z2vq2xMI6ow3B z5rXRTytmi#?Ns1_p$aFe^KvCApV#$e*T4|GHt%&ExpIxq`+78FM+URyVLywB#y*So zv)8yF;xa%emW+i33Od^AM2+qW>(X>D=+59~Z~gh*&@kuPq#)-Ybjm_*mOr_xu&zkV ziaAx?v2_;kPC>uarglOy!>+i`=C+;d70YZ_^)=-Wyy~=bF(M=w0NNrP~3D5(z%Z%kz|ah z(urugNfB7X3wxRoxdBzg4cS*TyU~G;l41Lv!%A?`sRmElNZaM#Li>19tnq=vCMY%M^G%O|p9s@aXM z5FghD9~Nl3a1E*|7( zzrhHi=jeJrDJES%R6g}&AU>prxjzX*8_xn3;X!MfB5*F4BqzTiGB69gSgaY zON~aqQaNIRdzc{o(5l!6ivZtfBa#`qhUz@tmfDH^Bz*K5^@!Y)aip-?nrwBN)}W4ej~nT-%o<>$3i>T@ zxR6r0a(?L^2xc@%M=MfpPmRRkRoPOiV@~-d+)XS})c$ZFB-?Id4Gq<{tR4oL!~Do~ z({o{x98cTRK){?2awW4hq8!IFDj7`YRvUKYx%G3tZwvOMeJk&0o%_I99fWpT;a7eO z!}nl3Tmy`g>fxNj7Wahg<#3oHOy6yGJkKRG+7Vf#lGqKZB`_`9DS%AJNR*_9#;S=L z*sFN5*jIMs6 zX5u7o7j4)aRdxp3UVb%-o61e0cUfqLz2}Lw?lZoO*!Wye1`p2KHN=_kRRMpxaoCF8!`v^?P%1- zmIX6goy>>R(qWplR1O&~()ffNt57A3<9;dwfgu3e!)+BlMqnJ-Cn|E0J{!r%(Tasi zvCL^xncm{w!s?H`SYz2S!YO3PF7ykNq<6u3TO7o2mF1EADAFz_nw(E+C-L=s4w8e9 z&;HEE?l&_2|8YpKW~IWBtwyi3>iK?#Cig@iu3_VGNppuv60EvdY$%j{O*uBZriZbW zeMY^-`gna=>8@(M?Yz|=h$$k?$Z$N$cSgzS1lRo`PmZFeX>4O|>IRvcQ#regP+=im zx|eGs4!ghXv)8`&a|(x%?Ar5%&Y+XxW^OEaRH zF-yp{;OvQ+ARVa=lG!hchqDCuJXH`^)q#t<%XL=Rc&rC3B#s$djTIAPOFJ$?HPpmu zbdQkY7O96lD6)(!ij}0~)O~(G&rI?b#c5cdG1?=P1HveIgJgBw>7-mF!BsnRkQ~Y^y27T zmxI)?vrJ~pCTc>{`UKjyq-zibH*@wm89KH+zk!4OQa>1)@lEn)<^;B22fIgzbq^ zGie)g?(6kZv>F@quqPtqK5OF}v`~qa>~oP#m>L{p>+@sdVWaM0fRI^L`{Gewi# z!=+-2D9a{^XemhL7hW1^8f-DuDbh=vQz$%;);g=gG&vtb#N^b6GO&Zjk=N!XldtG-M*(@cw#$7w@?CXimeQTielH{ND(M%|r4f2D%jR(`BqO$SuD4pvTp(78^ z3A1XWJpvwALpwVkXH8#|c)H8Nn;JA=A`H$q6UXfCyjmGKbX5UK%`cVwL4LYIYPVT@ zSqdEoOf7@75Skooo6|iWNY%L*&}~(B0#?(;$8}^GL%?oA1H$L#I=hO{Ap*ywlC-GK z=F;vk3LVRZURRKWK9e{ecA<2bpN{-hfj>qZc)%HJ30OlrfWd(7aYOpRbU0x&L)n)* zW+Yr1rq+y5p?9@O%nOn>_n`rKr_fEFq)AsJh-^HHG9{;4#3M4P9=a+LH^*aKz7}9l z$yOxtimfB1?4n6xv4q`|0UES!HBTAjgBAza(TZtP~8Y_0*{gBy9Zv#BIPoBbv> zge#T)y42cA5h4vBC$GfrBKH$fP5Z)Xl~#2qt4GB6ZB7-87$SIu+jtSCrA6UHF6uWG zrhKt}IrmyK!d)%Y!DY3&rL~C~2oZ2~+`knZ3x$Vn%pEY+E}lY*g$SxBi}PHSr?)_L z;s#E8vTtqG>DttnmXquE)|uVCNqTO-rWZGC6|u@}W0Aeb>dpD9YQyE!N!3_EW%Y zL|WQ8x`0isDp$k>5&YV82%KgyoQY06lekwY`LYawYO8vQ zZI+>dAe3A2xt5y`v(r)A%(BxmQP!|#f*CAE8{G)xRoTEAj;K)YM?6rqo^zLF(a)bR8Evwp+Kt`5i zw+4g#fx|Agaxh~R;M@pebgqzA5M`V1&2e-Uq9XHLx9!0D5U3cQ$W>7}3I@wK5&!dg- zYO)T~Ko(lLM`!cdHGjToM@0jolwKrcsJdy9A8?mI%K zfzKm!OzsnpfB*8`2l!X>kMVcD^Z3iZ_0jvXbH#}DR_N-H%3-<(2b>6+C$&R|0jW5d z3<&NVZe8)4bEA$e8IGY1fXIl&G8#=&$U?Agd~-x4GD307!}dV)GUZ`(GIfh_aN3v3 zlb((*&UF78t_GRxo@_D;aUQ3__NjQQ(;(Bm^MpG?wjJtBmIO(S3}Q?;GOl8=U?)ct z^;%N;!PSA80fTd22^)_~2Cb7{NiYR&LDmf7}a#CFmGBa12% zQJ^9Y_(8(vlaIgo9r?#U|I)`^{?T{+#W&yXovNH{z;UY()nC;veDd*4-KK{G!%0JrQ9sbmj?EtVo@>*nwrEv{LtNtlI(+KhLUEdAd zZ&N&5mHShw716tcY(KY{s{`GJ#9b99H;_uD^fU?R6TYn;Oo}w>)Ipm;)oUf(md-TEC1jQG$e5FxDXTJi zy6oFG-+bCu#*b6W%Yh@cDn}o^>7*ehWxssmB zNc6sAxGdOZcC=2UYr1_MhD=?CM*VdVbq~wxIhRaXVx^tj49JAD3qyKyJY9N)E3pG{ zts@2*(iqxgCL7%HYdQ9`AE&~dC~^#Ev+3am>4j#AGFmaVCF8_3^MmqKS=xORo#?TU zCD8ot-eB6)5E-oCv0hGB>%#F6y~b_VYz-qw*s9H~z(u$q$?Vz;s;uk=;;51&$ew{g zHNI`wVWj1E@P=6hKqh8l%i6v&ISSOH%GP_?-|sF!B6M&@H{c2p)=UpC##wBIo)Z6*U)jX)9Xol(g`v^w^bZl zGKE!3@B8u6A!0?XLd0u}P4JvuPYIMqz_N4;RS2|^Cj`kKCI`pLa0uqJg??J}n!pn6 z?)TSLqe0PcR9?^3@BTvmi;J&&C=dIu+;XXLNCRWBXURmfemdXJRw!ij$Ma5+iEcDU4ixe|4!3r0UP;R`G~t>{ z=GyUUJ~vRija$!5qu>baYbP!t##69zNRuWIJcdJa+_RErGr7zo9%#hV#dv!jyBdcH z>l$qa=Fqf{=FnQaFf8`BfmDith^D$hS)Y*SwW(zsvth<G~KA%A_`7Jn&9mEd|B@CjY2? z|BrsGZBN1-iv>+U?LP$0brkCWu9=crg+Kcj>_lpr=i? zl8i@Bpx@vu&AUcJodewvCwp(EPnGdG)vhzZwJ|=dLSTP5Y>fSI<}h4XYDJVmx=rnF z=HZUjnQ?6lNZWHF1<#H8rbS{AUm(L0X_HeSo{$%nh%5kjXgTQsJq!twL#WgCm({+P zCL_pfC4-wjw>>ZGJ>jrFoI{3{!dv@{B_7LwvI$SAZex7jikDs%itM4{B$g=_fNcjN z8r~7SZ%|}X4GW^uV@5g`b4cGhd+J!p_{klj=5;k5EDk-CU3X6eV1|GkT`U((6z1E-h+3S5Tj%buSJc^k zG#4V_ZA~0(^XCqf0P-ya85vfjw4nNBJ921>4RbUY7@Skr&$7}$=c2LQ(r0Q`P4VSM zGl&y1@+k)I`yo3)yHOP)$fTH0}h+E`o02+^MnOH8QnTBCK%xVR~@8 z6!{q|_i|CoFk|Z`g_(#LNUZ$yJc~B8-C)a9-8CwO0eO1gPPi#q;FI%faUZzm&N|4$ zx2_>kQ_gzpD2%jDT)ti_#pbDg$c7|rkJl^)-l^LQx+lbaeC;yOsm19FMok4-w$^GJ z6_5m;kWyto7V9@?GzVljwtdMNbU>zFd;M^K{)h7~|JNToz49Z(8Fh5vMbG2Jg!X%#pC!B1TJG0B=@oPU0-IZS4Ggi@^OH&%9NQ45n*D0qE3pX(p zpKy4&XU9aWJ#Vh*6oT_4vP|ir?I;*AtX;OZZs~AVj8q5d#}h81ue`Q~WOibV1^lXR z?=X}hS{!7bCyO+e8=Tz?zl&@rCS&#{w_a*Kd3^34(06X;Lfw?z$Gbc~)5**=8oyA^ zvXT`X&B^XAo%*313mHD2@Gi3iuOMh7-U3LY5s}90f$uDdP0^V0__@!=X9VUP_b~{& zQYP+z?6@kQ>$^2^xf+SW6pf4JtT@88ZhVT4+PRgZBy#uJpk0u}3_GZF=bdRcolBl6 zkDLJI9OC4}nQc^s@I}oI487xI1~gL?1vzR@HsHxb$tzxwm0feH21?p^hsQf|A4yrA zv~+099cJ9A^?H&ej;5h~DFuzF8!`{RC*A0hDXesDVS`HTE{$Wyj%`tgO5s#oP>pfw zQqM<2W@@EYmGlr}q{cLTitrV@+8mn$;BXj4_t=0dia}*ke;EXJ({USV1$pXAaQiVy z@Z^#{1KMXR(CScAFv{jUr)!YQHMG-kYn1Zqqn@6jRGW4V&HY`e>7+U!lV^HRBd zp7xUZu+~u1`|TyDX4c14F{dZEC|5cbID_4_OFl}4nqzmu&)YNijrUi#FaB8m@ijm3 z&YccCyAuM%ht;P)&wcrYzY;w zuU%FNIPgM%iK@*flQ^GOqL+Ou%ik8LxgMr7GcLNw7**wLU#JEh56yLO9RnhX#kCg3 znuYW1T=|+16MI#*_{oR){MV-Q&*UNevY@;ZVE4mcnShRcGY;B@y2=%Zp+37FxH` zz`8k#k^Ou;x=t0Bu>)ulc|O)Ha>s7soZd0D_^gnYqJcZ*H>*Mt8y=fO=0H(db?);% z=GRhl7oWJEHQJ8ay<%p(5=UgX>MOmSlE`X6v9vxLEL}7YYNHMyL*ZULk1!n!WtFos z++X3&f$=WGBfMpX!r3{Yq8X0?fGsa_D%@_K3Y%d9I|WI!Qc6W1KtQTlbJiF;11gA8alcXfV z(vpPc^D*IaOqjrkz|*}D(?C)t;u z{h9ye&9_LGJC{}I9F#)8HvHp%%)b2FKl_VszO_t-IHl3d;d17+q4G2A%U}Gt_8V_S z`*Qzok<92auin3KYa(|Ko2UV1mrx8WddGmMzw z#-dmlfF=O!ueU0<#zvtVkmhLP0y|x^9aKatLc0;H>va#yY1dMF2~sWZLYQlRieY2O zqf%J5P{|yY`KE36CQ&Clg90s8&V?aXvGF3I3CAp-;t^oiK^GzyW!=z_@7j60Ww*l4 zu?MVM%$7W3hB7OpIEFJb9erPvsBv)}$qbWOD=ijk{gq*I?x-=MY#}+b;4_pEn`THW zL-b;|Dz&$rUNR=q`}old&S0^3DIYItj9Ot9g1qrbtBdFMle8PRc1v_xoChAqNGt?J z(0Q^B%Stw34vG*pA=TWjXm|qum^~Ix&{hVg`S;i ztUDp+<=icBhNH%GoVoJH=3$v4^LMbv*=<^#)bLI?#Ui6>FCC7L`G)5OwNe>hDhQL~ zwcqx|;P6RoJb~G9w3C`6#(9wFAsr>~6vJ*lV;U9}D1ZJSw>e(9R>C53fn%)OT4Bue zyK;h^TQ3tKV6em%+l+Ap$?&ab}n zIDY3xKN{*Cr?L~SO}oZ;*}ptK_p8tEC1{!L^7JdYO9im$Qb%mM$RdN}l@aTD6vz78 zONrzncv8WXQ%f-NH_aq_+5S34F=eDXhqu)vlnxGC(abOg8l@JQ6D!fB*FgncmPZvc zO|@zxDJ$0EgH@TcdX3?iqaUJn0GLZ1S zS#&aw(Kn^+Q*;o~N3M+x6dh&p9E(|G;F>=Ex0fG(bbh(;W+wgk&0mA>b!>1V^3zOX zro^wJz>TC;Co^Iqes^XvjZ;ot+Od9ho#HB>a3%*MOI-v zng-gzV{P7PZn#1>LI8;%ni^HI{Xt?2+?KNvYVOXrUhKB)?Cp}hNRJ6U54-pNG(;Ah z-7QfiN9$@xEkH>OwN$t#3=Xq-GLfq>P<;@}?>MeLpoXnBKMAsPqcgy(C32?fU3Rlr zs@=_4$FO=6MmQfOxS6&ga&VP?{d+U0CZTkIG-=~?DHf3FpX z`*Nvdj8oJi>VU}vjs}uK%GWw(`U+0W`R>xIoCEd4!jpyKTs<_pd%2)<8_TV#64OK& z9NY}RPG>&&8a46hS+ zh^VdW@spO>c%sN9MG(4}PD6_Z@~%#%1UqHQamL+blzPdQ92r+XVSajj4th1qo&nK}||FuA;<>@2?Z? z65_%`BYh}ZTZjtHa%=5$DQAZJY<<-V6$Zferb@IM-q%mHaE!-=!JwL`p!MLuVx}(F za=iHA_K9$+PQ~q`3i0^q-+Axx?ys^hf8*Cb`s^(}V`qp;MULI%*ACT}e=Ywiv;A`R zn?L^M+vAl~aC`u!m0s@EnaEE*zWnR2gl}*gT6Bg~;^LT{sL>%)@L=Rvjb=T`Vt_L) zR&$uK$HUtcSiHstF`A&jZ4pUgS^9|ASUD%5eS)^he&4C}*IyOA+37iKNB3!Dv?v@c;ck79RJX`RGgVcY_f+9MtJbvN}1g z<4<2-Xa08nCG-1V|Lko8(z9bp?#{Tv@_n7*U;R%08I?}HeEF5x8y^-uzNfKbHO>j| z-^~B>d-=!WTi$v3xj%}%@iyb_Inaxg1*JlUUVrcPEBSkd{L|-=gYpBebh`N=whXfo zxONjegL60nYghI%_Pg^j72OxYbR|IYYwyE|-@!p+Mks!Ap8_sSXe*zrZGtxjl(`cd< z{Cb*b{g9u6lp?y|^*vgeqyFSd7p zTk)xU;%sGF+)g|kYvz@c?-Sev-h>lTq*k!+nF_ug6vnA}vzLRX<+_X;`LHN5@6okw<~k;5Gbi=M6L z;YOETUYvEdmB@=o4Q+BjE`x>T6G7deTvzD@YOQu>mx8RE5p&?0y&!j0yO1PO znl2&3waXbhHw3v{)Po%tkq3N2@5R!MIWW(LCa_B{w^e;tMT#!dy@bBClssxEpL-Jt3ke!xt}9Q}O&clMfn>HVb1OmK9V5f19_2Yq=za~sUb$I+vlpb zM)CT78!a9~tKBuWAYwqgs*7c5e@X7Q*F{vxOirK_a+R7T(eW_D7abu=l_kq=huCu~ zZo}Egxl-mg^FH3U4)3Rel7EyBN)Z0cL|=57gV^qe^)_VfQ7ze&2_=+Zh7}7*QX=bD zBa7y3v%X0rqZ?jfuJt#^VMGf#=lm?3)AVsvj&eS~cB{ndAPy^32F6ZC2s>XA3QPwl z6^d|tHbKYJ0|Z{T_blNg+`1eq@yiR{2|H}o<^#d1G6-*{1sx&Qayz+Y!$|~7Q)Gn* z!gcmKGJIf@pXRF0qOrhgh}+8bB+MS5`$VsrNfoG4;uwtcVDA3EZGFqD`&hOfzb;6) zb-O}3-H<>Jy8M3IdOs}7lHZnX*_NNOWXqCm$(AKc@(aR+V3L0zgB~0LA&^nXB#==< zAQ$d5A>?n!=9te`EK%MxU#SeBuqIbrraFSW?9T02saq53A3)7NR5dWPPq{~^lP02JdGSZRjITv@_M@=mzYFxZB}<6j(wzVm*cUl zfC(qso$t-cqde8fI+!>IRSQE#9C!MznmEjgH@UJQ*qVm72*80vg3*w?F1N%m zctDTs@!`(4!`+BJlm>hxFpsC2W;HeoSr%J4d)Vf+XFj%Lw9_$Jb56N}J**pT$mXgY;t;q&xI=9@2n+y^=p&!^4jLQOxVWp?A;p;Wbv0cWy^r(eJ- zx8`Q@N0LNN$qZ9P>|iTh=q+>QD_-8E_TXGs;%oyp6k8JY+bOthip8A z6M7tPf)#qPxAS<08W*m^)pIpbj@MlMMshkxJP)xg%GgS|5)rc%yPbEg_Y$R+h8W6Y zmXoWpb|o=)md18#$#LpI6z@4nH3+d?7r^#vx?4=DsB(;4n_MT2S-nN2zIExvG$%&ql9`CoiK_=ISs&YR9)5ruyMUNhN>3!Z9%XkI_u1JqIff64 zoH?td4w+?#7>!7?Hykz5xG}L+{A;H1@t^+Am&!K;O8$QQ*S~$W^3uv&4>@%)LrO8g zEIe6r4@$&WyG{AZ7aEzBzbCL#TV9KHOud!I3Yfd8cGV_Tj%%qNLiK9;Dv8TR49OH} z`~vF*4;ns+e5HBi6X`x>1D0s3gBx1UFP;mr;dni|Ok>-5|Gb7GuUG+zPvipF(=x_5 zngeu{$@S@(R$^ZC+QG#^^jU9kb7tjzXOi*KwRXSx&{lW0gRQ;QqiMh!6ZLjVY|Gy+ z!CR)tJBhgISd>~3|aZDNGo|_)W<_zfuyH)yjE1X{0k!@iM%7tVb9>nNA50HYZ%~-E(WwAy)0x>ZUg`Nx~D(GSaH79vXioYLihpt&MAe zZHdcAuq;;hy^hTJS|dR#kMZ=99mqwsWQsqUm_TrgX1C2i( z9USc5?tA(Ec?Y9n3N}=HFaU+{g~nxQO`u(+d9Tb(quqZ&ToXOPb|U5iXKmo}*B+J8BWG3Jrp!2$FtB^Upqe+AJIa z?mm2IED=t6-ga+1E<@6!ARO`5JZew)n0&Y_C~B zFwkAtXKo=>A;mFOv;bsrtSM9nQAzyNi>h!+g82=fx1bxlFvGMVAjoftlg~M#>f4R zCzj_WYiC;7lHI;mANFby*|dggEIl3ggL$5w9V7no$P9OpiCuXGHEt(9Ej>HT-#SiP zD;DHL|NNL#kr}AIh#kg-xu)3l&ec7iDy5>G;P*jhlMu==;G1NuKYH$hG&|{agl%bA zFz4NP62dH5JexC-=w_5#V8~(<>v;)vkrCzeqNj}Y1cSM<&L!QaQ#3DNci|cc|n6!+7NSq(r zPLocE)8t66D3cYXF`XEl=~+4w1oO!w-QJD@i89!p#E#)hzBpDE=JoJvG}pdmk+dGz z(W-Y`a*6t9{qt`={=>JQ&PjyJ<7g6ONmGScrkp*p-|~L3fsx7TK6l?Vmp!l5bEXM) z6##ZSCnxovE=A*t`9u*jlOzL(*vsZ`ADws;^YMj84N@rJ3 zChuK+H`$4s%4ITZAY8@U?x~!9jNgKoh;(Qq;93Ax^~j71w=GBPbOY?WPLYX9dUI%@ zuR1xjFn+PyE3OT<_8JTxL3Y!4T2o%WTXLq+eTu$cy5#A(RH`Gkb9FV}sWU2pFB^xTh0&M^^hT4Nd1jxgg| z_r|ZS!X8mxnL)X6mEecAyrr5g%0mRULdD93Tdk7-eCJNw^P8yxsNKk*`OIyb?nS^% zOr2kDbW0}3OPQ$*p7Gc^%iYp9%ftcYi90U&L9#r)-H&Yah*s_At|m_#YC6Bq2$B1) z{oo&`_$m>-mNJ_cD-xv|SU`&q?tH};#~Tn^Q!uE^6q7Me?hG~%x$gxBut?qEf^pjv zs5_V5*OQpNH_s6@LAMy%63D1C+ZSrs{vjMor|w-SHBXC6x-pKzk107J2X&Bp9P9h5 zfHP$p+GN4g$_gj>30$=D@#}&%0&(%=b4ln~Aeg;5s&ARuWwX{jbus+9P4n^X-}`h| zB;>AKYzlY&q^{BdtH{qXabf0Q-F@ZA+I7@8ETEwrGPm|^Cb~fF}xHt&bhf=BYs&)>I$F6$x`|0&A zTk0E;c_4MX-2`g4iDRVNG;;0PSj_HPxhRc$j9bT#%>;hvrHtI7PCtisJvi(~U4N;# z;{y>J5B6w{QxDx>p1!K5+;f~g18z@CwX&Q9xTP7H87dAjaoMrk_+Nq6#8&MqI3R%RD0(q}`FuSzUgh|AaoesNw+oX5mA zls;k8E>emdxdEauv>;%6l)t;1BW$OO13iW`&2CPJNL%I>L05c4z0a;-Qy@d`uFyu) zy7Q%3AY%@K9oC!9aJ5UPAYY)Gar3m#Hq4_dlIwOXIg$J#a*ITDOK5!(jDJ6y|LSA+ zt1m2AM)?`P{Vtf*+6^0|o%EzXtxltP!WG)lw35|JKrySYjQTnAC>kCz8n&fPy)re< z!>Lx`*NxIjrLcRUY5Hg<9=E33*@4a`nH7o?hi%T|xQTz5b#|FZsXNMoAU7U3?&x*o z_cW?&no|(wDInWcxPVRcwa3oqxs*T2q$k{^Bl-Gj(C#`9PA#^!^uk}W7U!NsW)q(` z1kCPEHg)NY?6KR#YsiT$$~H&!DWKyrH`bgE`!KQbqRVRCAGKCXFbJ`WRBK`GQsNkY z)k}_NF|pf$QVr1h>Dnsk~iV>Vp%uZIq_#=e@9)nb0DAl@B&$5PQmIvtXiH z<_d0U`|Kre$#ic8Eh5F|tv;!x)Y}qvvq4mYsvVSQPeHk&6VI&m;Z~>)H21h9t~FxN zm!~hg+$&H^HT&Sv!_XRz>Ip&FyA7N_OxVS$8twSG+(-)WdOGSTT1#GPx0I^fbjEFL zve|vLI~fVgXxo?CXJDD3+1nlvkaku0I^a*tSA!|S4ov|mV342~@$ID8Nx6_8wPO>5 zfYZWDbP8r#MloBFbB=(e@9N>%9|mX{ipU~7R*pahY2@2oMjQCW!=f|-avjTzT)ksg zZ!PvK4`-)lvKLJZpjOfuH@oCuAqO{UnA@?=eIOy30myS!sx8Md_-db#dk+Y;qv>Be=G6FZ9CEZ45+7zRQ~Z{kKsxthwu$tXQxCQUlNX(MO)cssk(S-p(c;;Dpd z1)V}x`wf!3=S3s_OzD~E$u?*4V3zorRs7fAe5w55KmYa%`^|S=5vjs~R=pLuwfFU8 z=^ufU=I_3wzxmESloOL7H*t4RE&ui8KmRQ_`QM{oTEFD48yntN8LOZ}?-J z0H^*k_htCapBGETi-R45di;mG{$m=PI+ngfe(~M$$oBe4Knu#DsUNcUf0p~g{^76T zFZegVq%Z9b7`E%tnf}AnGFJkl5sCgh3VfDw`OfYEGJ9ntw({kHZpBWUnVo4|k1D)o ziyghSD4CG$X1u#AF6Y*u+T*RU-xuUeF9{9!#yNM?Ch;?`>8R1j^L%_d0#YgGY0=Uf zh(#zv4N5iS#h}?pzJh#g#kKBKnm=zkpu7RR7U(d>R>mqwWrRIdM8~sc5o+)&svkdH zN^YS=99HM9wO*Egyt#{6J#$e*#R>e{PEho2$-P{x| zb&5UvaY_$Tgfg%P6;6E3a8T|T;p;o4nwmDVqs5+HNzL^QueH>YNaQd4(Jj}46jp_% z(8!esYEPGjyBX{H4ZdgMO56sKD7!ga@@aMhWO1;^UQ3~3LZ8TRMAPf-MLH!f8ekI8 z^%wMIX{aZv02XMr#nDuhUU;_9({<*~ZhH@Mh_pyb?mXDUVoL&HEuiR_h#%(~pC(XC zU7{Yb9Zk$T+ic_G=c2GkB32<(BfqP)^GAnTwoyShV4@+sbLdRr8F~ zW+#!RS~MfNKOW#i`KM~JM!ONK%e(#wq;TA( z{qUHJE!0$LDeWu13gyeEJ&>Vsl!q?H$6Z3!_NaPtZv2(u<3t(GCn7*0jlmC*K6<^i z{2V@-h?wD7o7lL6bv8**olq(}G@7>JvNz;Tx8A%8IMTUf_RjWOfaD-H21Xy2BJ0E+ z$+yg8`jZdy@o(YJ6bye)!E&H}aY%hS{UDt{&ueqnSdBn!x<9Y#nSo10rrS%W=uSK# zm7I1xqv?7}dJk4Y*!-ca&!?A`U%N1Fl%vKIAw{5ZVoehBh_=oeL=`1gM^LoxUWhDm zF^i)YVNVjv)AFp9ALZHp2_cP^oOPV&6Rf>5Odm{&g36=r+4L$gN?TP7c&6ehb;RzZ zW-+cDbHp`Jv05eu&qKE73$09B%%Aq7oX3n9aWl#-#S^*Y`atJ-)CSs zj$TE_Yu%~5vb7OJETzfW1Z-jsXnSm{li0ON66;6FK>8l#tf%)m(2dMQ=G$?DrMPmm z-EK)+?I;b*@rH>ePLcq$F|6w-UsbG{dD42NNGIX8FBex!r8?1lPtQghB-5@c4Pra> zbZ)ib$kMi18SGvatK?NqY$9>s91aH+3>CnAXUarB=Z@m59+Mdp**$fiWnHy1dSXqa zhc%dK0OTj#TQ>`mnSJ5XH@2A+q&!*`z!wjm534Ab<{Sz}tSDe_v6JjtD=~mzpxyxk z`7)Sh6XJ}6{DI2tf6LK(TI)ql<+O95z}yJCYi2y!3Y>6{d2aI=WpibVUI}ElIbB~dBwcMX-63*nI zh9gKtEI5?XD~LHlePhGH+e1}fMUm6sb}@&Utv!@#dloM9rRxoCDha)q#T&45?UR8A z7GT631AX z7PE0_fvh*!Vt!VV+v_lAiiM-T6eJffUy60}{LDAZithWwbJKoCq>Q#2JGB7F5M^jE z<>TnGTq#Hn3u?h*c97fBJXn7~@`drmnD1?D*t;A-Zn^J2R!J?3<3N3EO3PS&2V{Lt zfS!1}uFn{!5PGKxK8B@~?kU{#@Fp;otwmzxdrt@l{YddWD_bVS&ubH30#+0`IS;#d0J% zZKCICdX`nIGR#~#k%w~G{zV#}Q&Fc96V0@2%m#B*3fh9MSPJcL&dYa zVBo%PvZ%=6W^=GjJ=zFyVdDxk_wD=aFcCJX)Js*@wTUQRb}_r?`>O;=6iB^zFF>uk zzUe=K7UA$HHb6mE+wyyj5y858Wm(l+t;;iqCm>wsMhlIr`2@|Jz?vw3 zHX{A)AQmUnV68R}XSl{*?v}{$Y4Py9bsVV6VXpRym#i5#rDuHA(!I0N>-K9>R6Z(| zl5E#bscYkEu&u^82lD?$jBNBTpxn{~x@5aYuv)|(N{h{BW~2C;VA>j=T)`-&P%(67 zuANj9BG!(Sg3|j@(nhAOns&A7=MIb4H#m?S@p<$-e0fvorjFPQ*b-w*dT|w$ytGG6xMgRutD%ORs|UT3IA;n|9cu*Bd98CjGUZOPJp~e5UK4Oe9*g=E%jRVZX2PgZw38kX!>L>Z(7nkpSC8ER*Ia>l|`?}^6}lc1Skqjkr ze4fBtky+ZK%2;(3rzJQQ%*{sBF*@v#aN>LXLX3)JcJ`pOXnqd1@2~FYu!QDdV?WUZ z%9#n#iU=coFkCvB@z?62k6%{7WaFQ7zVKiEL2s`gOrwE8nR_WaEK$YJBBNh_`;Tv? z^)2*0Okp4YqxJ#h?&&lz>9mQ3?W5`#-k@20@{QV93X> z`d@y{Vo(S>MnEC#7zKr}wxXv@}VE(9$&djur-duXjts5K4wD4MRBjjva*hhdU<#}30Z5yFnZAMo(g(!{%^VFV{aXb~V9 z^VU^hg#P%a;1Mqv4*^6`{N2(pkc|zYMM*q_7NzkJS`5ZRmc~##Y-tR~L)bAA4`Ih> zJY;Dc#zGCj(U0rs)6!T7J5FLD>^O~uuoLjdUwu9T#X^=Qa4dwDAh8fyg8ul`_|wv8 z2p|c{Fy225iK8L3B#DO5k~A7ZONHYfOd&sX_Sa8>Ea?AX=cjSx{WBpDLWPeJp-=|K zijdbK2=IlUG!DEUf)G&m^4>ZG#Y08|_l69PU{uJB5fA`E&m*uJnfEe3f@ zsFz?!xKm*$5o!pA0SEbs!+nCraOh9HhLpdD|0(*luZ({%f zy3@O<2@ulXza3EE4y6?&aG7^2fSfKg@{+*aetgI4j}rzG3}x@;BEg~e?@8hi{Nv4k zx)-N^7`A^hmVgfz$~sAK^Sh;AB!wcO)&U0zfuXTbbfmz@@!nzzhJI?n$IY-^^Y-=s zZVAorzf~)spC3>Q#z`0@4|@Wa9< z#=>^TjW1||SV-OMCid1&8^CB?J73{T?HX}w#Bczkc3pcnYt$(m#6^pwuASQJx`kVQ z-}pDhrhxv?qxVpUI^sF!d7t+=U-`l}{>4v!<4^zK>yJ-;1O8g}abn+pc=EuVk<&Nsbm`7_`0{^N(f@!2E!ktfvrre}|@eTcs8 zBhMbc{LN3X|0zJ<_bpH8AAa+*hyLahI(*Bs$2%YW0DbZC)xUc8{^S3C^S#HHzBT$+ zef_r|eebb(>jU)p#}9n#^GD)iA6)bKNBynmk3ahmJ^R@6$AA6tr`YcY=>1PTq5tIL z&mYydpOE*7=Z{Z*@(Gb|KY#qQ57A!)=u7WB#eU_J&mQYfJ@x+m?>u|N-hGLH_uhZh zKlSYKr|-UO`4T;Q_u1o@-g`p7AE3`a`v86Mp*?^4+e>8`_a6V|75dxY`;Yg&^V#DUzw2eom*{^C(7*TH zPv{?f*Rx0E(@)6#?q`oreC7$IKK<gDtESfE9V-in4fBX+0qCXAL7c)<>U!tBr=JXT# zeCGKB%RV8Ee*XC1KSXb5pFe(%d5V1{K;N5tLO;eldnoKv@9W&N$49xB=;i#5v(FyC z&An`SV!!zK0{_9g_aDE+J$uZAC-iy#*&{4IAx(Jp_@fWe+v2mw&q`0RuLS6?%TMUX zq@d^g6I#pfJ>D!l^&HE;_xP<>=;bK~-&J0=e2M<6!t=*eeL|m8p8wUoyo{CA z=dV!@;v+&2_0sV$8JBNjLmk2|Gn%8FzU; zrmyRXBG(J}Zah$v<5tx_ujW-IM&mF9#&9YXNrTgR6)xSNMlPwUNCZg=o7fa%lwE$_ zU-q-Lp*?^K*r<41Irls;54A{Y*&ark?WR&Y=VF;uxKFk_Vmvjzl;cIGkeh~Ossu-y z=DvAR#@j5G6t|_R!Kf;wuv(6p;4`z`?ljZD`W!YULOYVX9A{a!wydv??Q~67D+m)h zP1q<|=b`AVjG-a4EFHShdpEM&@5hmXfhTU~C_lZCGusnx9jvm{)S*zwrF&h$xYQ6$g#!*X5 zy^4L=;vU~)Lib3)vLeYyvj7hd=NX9N5tGhl$^~TF$1&fBl|cq|?}%`$l~ zzNu2^K}(gva8-7D`Ci6IWaDyM=cc6rR5?zV-AzMutaF`5@p|b>6ho@O&$H!F)-MjB zW(_aa4PsE!#j1Y056H^ai*y7hfx12GHjCG(379YMM6(h;)Z50O6Vu(in~cgfqMG|2 zI};J#Qd2<5jZ%_di1$H`y%;McJQNv9vqzJ}AhieK#;REvWW&yMU)~GtbG*t;v_Thy zGb5KEVL~}`M6yyvqB*uU%a_*LdDGIP2bx)p^M`JIW2;dm>F4MAkqqlH;hgYr==y5l zKHK3Q?>2JZq$Q#U!hEzF;A_3d_cT&54jQ+OLw2t?)pZ%bfB{?P;YvN3oEpbb(xhO6 z@<@7dTf~4)25~O{eW_Xi(T=HKV$^Sn*3XyR~OCmdEiyr=BBY zyA3a2M{q30t!fm(NX6z_rjACw4>rL3c`7TDTW_uOQtRF(Fai+8r)H|um~_^t zTUP8@93N~JessRy&SNgJJof7aTEHK_+XR@Qlg{@i8rqH%Tr|?iEjI2<4wa_K{A62z zD#Jr{10dw=!0;(LFHlY6Jl`zoDiwVst#A8A(#l^_kD8Tyo26TPEI%~6sJ!14k}>Y_ zx2({|3OdxQVCe#s%f+*chx{AwKK`)Fy?Cdkz46gq8f-2st5w-sul}Du8*uj&{co*e zf;?9)#X~53oi$x(%Wa(fi007QtRBu00=v8#rSBi{_iXa(miikJ6K+W|fm#bpOJG{1RX=1PmqH5=l(gMT zB8+2;Y)t7E>iH3;Ra?{awohytD;OSMv}$Z0tDeqznVCljbIBQ=)0~HD!NW|>u{x`A z^JXvNPlCa2jyLGaStRcG8cz*YTLwH|U7T5%Kw2GcYjNN17z3c)SNTDeA^C2sGTBfG z%xmcOAZZ`@>yRf*gGgmfEiyoot8=QHYe7;bEDq1>xRsal2WA~&k@H4{(aHX}2syNV zDJ2o1zg#I`!`R@)bmiyn`-m~m?Dm=DxaooC!yMkWkLg8ce)PfvgA3;xb~jWc*u0l6 ze_yQQZa;dA6Q!CjO)y0$eq93Itf8&Pw`6CrQ)o@ww2-l2lioLuGfq8^9tL0UU z-z&8aASQE(!#P}CjY}mTjL+0UfPdY|fG$cdv}F zfz-<`lY*)qJ0m#}n+TEWbh$=JFj-3IXO(h;DZkI|_%aZ-ysKFCd-awQLfYBXWnolS zZgd*!r@0(TO4AfgrNHW8Nz+(dr0<#s&$hjMW?K~XzBl9|Su4x-sQ$X(1Dv6v9cTY&v+x^lDIi=(5r9e`G(&t9{>tuE z_&&QOWro`D79v-sOM1!Ir*c(jm9)ul5XvvvDGZ5RagC@d&cQ8#o~8QOxfdpfl#0|Y zn1XQ9NOh@n?us88tr&$4>}bN?^tDh3<@&(JP4K5Mc9J$26M6rK68^ zxO=#m@snc_7EtdPEr7AwB9r99ER4WJ4eU0fR%W!XR-L#IjHw+RbQgmviiGJ-1x{vj z!TjF&k<*1}nX%&;WKZ~I8%ffpgri*(+N&(1h~ZvyF)PX_I;o#(Iep!C-RSWKn)7UW zJS*Rarga;P3#J_zLzhiz2=K{E9UchU$45}fK=F*c`jfhJ)i7x~L?iKdD+%C8gf4FT%_7kl*->~|@ zZ1>%J*DHsVToflO)Bav;w3t${53oj6lgBG*52}kebmL?8!Zjz{6zmr|n1DcboFy_S zdRuBX4HWk==Dc*w>k`V==x`&NL#@Fo<;P%7Pr3j-Pi`71r+#QdfR}-{(k&W3KuPch zj22e*o{wv-BR;V8b+#<;BUx!_)p{@~0gEHL1}S0BM^m}zl+rQ$+yvrMxB~Am-%c0v zN0;)9GQo`ugMw(4cUIH&t_?4=Ui;8APN-MkwK_@O9LK3lOOF}#`to8YwseKTyaApT zLZ;Fl-R;v>m=7x1+6p2hP-<_-n8=FQ^HDU}&F@wUHM7~E8#qLrqr>hx^Ut@KgF~qCjobwchV#y|W-89;h%Ze=TGJ@L) zhp+q4)P<#O_Z()96>@b(wYw+NuD;T{nRb%!n|oVpGR6>^fZJWTOhlNgBuw3Q(N<@| zq8eu~AaS-@`{^ZGK3(;GF4%bBlq^l5USdS1#C&NNRkfsDYXgZ*)?MZMrmb!*wsu(M z^dkzT;C69Q=i51=>|7SWPUP>MGPK)rXvfGGnVzPvCPsCcUvD&y?G?P9<4y}RX%|P! z*=~=BH7(mflci09@kpK^7uVb*+M4u%@feZnI)9PqB)m>HTf=?>;+fV!FAF6pR1%k* zj`6KRm1tYXI1eHee~44{C}xH38Q*I)>4jXw4RxL-^Ku1_#{0Dmn9krQ!kfCuKHZ?8 zMqQ_Y46th0UX6D#G;8jPH7s(r$X1SZ-?lG|?P?Qn3J^CWop=Oyk_wn;8vN4!w{}@r%bT@-_ zV)<(C7&ccO$Id}?rD81;oSZC-v4-j3(Al5t(TQHo!gb%~4|<48DQ94N>rHA?bT`K# zypb$2{(uy9_=eR1PFQEk(>5z>$URjUVzqfBL@(x}4ro#7a$?X^vXF*0a*#A|hH}SX z@r_-CrgqP4$fAtas!e*Z+0`eScTL2vtNeP#8S)ybZq|iUCsdk2$==03loh{DSYw-W z7CyrT#6VDvNmw#9^xBOw$qSX`5Z_2)fz5YaZzhQr zOvS_5F+8NrQo^qhStuzj$XG+mr5gM3%*sYJU8Ih2i8~m(F@%?6=6$TY_-)Ki!0NGN zdAK$`#~~z6k@0%05YVI)S`_frb4CY|)Yxfw0LJ_CAvIjk zQ#hXxTGEv;ywk9s$YiYf)jr4di{$nWrgC9R%X$puI@=t1SSF~-#i-u<$$=NmhV^`T zJRNihmTxl)xVs*h*~zhLMLTF3)Naj`-0w@gz(qGAEGivdU2rFVEo~EQWZg_B3pLH_ zS|)KgloZcfRR|CB6RT#y$;WIhM&#Z31PhsXV#1%r6|_}DwqB@gT;{yE%XTMkBDejr zaJ2c4C^X(owH2HG8dOa)9s*xgh7$BwhLM1Z

mUBa&VpV{&V_HvqAu_Q{c+1{Hmy|CA0)}tY2NVM-nn+LV)+PkmI}d_+U&Z^x zWvdn}>cVWSL%9l-u5y-pCZU#B@x9Rf6rZijtYFy5ARluqh)vFo+*`f+u-S1ZG6dA^}B5aeJJAEprT^OAaZv-R_% zvM(#xpt5%%w!_4spw^$qUHi)m7j@cWY4!?DfW;DB&c$Ri1StE-dIm zI=wsDIFP6f5tg6JTW6@3wnKfxRhH`sS*_xSbbE1t6HbH0XEw;XEQMTK-$EY`Q|n=G88Y4SU-znxB|EY(A`)%Lo4?MoIZby_xhZfnEFZedo#23c?W=Yuwn zPx4!{uj?f``s48jlTlNHDb{`JjuHmz8w`dyNmS^sZ-Qe1Ig%_|DpY5Y?H+MR%J|h(S z$_}-`yp-vk&i8mm{lXxF&-3z|iMga8AE zITE-T%NMfr;H#NWZ$cG$l;#x=vPEP)yc9mwb}J{=?W)nkpW88alxiK#Icx@>BdIqi zgiA^_h>*)9SPr|$jkZo~>~tcnNvCm7?bo(*2xLUjvTLh5fPfU7kAN+vpZG?l3%?xr zyHm{U z4={iLA&mU7-{le);U4{DI!7A>M#GUT5be-ZMQPG(PEd8!}OvJqXgO+?xftEwDE6oi0B=qM@|{4(jotpNTmO!LI0Qc z>jwAFo74vwKI2c|ftQaXjz?NA2{(B?iNUyJO!7D0kDcxX-y8O#G+Y70!922o`yefS zCC2UZEqw$W+Rbi26bg2@?Kz?|Us-F*442n~|4W|rWt*shq_lRcytL7INbjB(8s&MW zqUoawQ>T00_fk{JvQ{E#E3Wqjm-ZXJdofq2NoRANl@=;u!FbzV>OlK1Si6bBoqyIr zip7n)PrZG!{vjyU3tMLfga`=QK>C9mEuVTyf`oxLht(Iuhx%*gwAwDJ!18+LEq-fa zo))3U&n30~lHjA4@phSvW4s3yBG)Y9&P=uRzMPg@zMAUJYz5U?Fzubn2dWX%HLtZ4 zO>pEG-yYk}P z?OqGe$Jh@yEo$GU1^}y+d&j-oTlzdY*t2P!<%`F8T`Qk!?eRhTRk4lwT&mXgF^I>L z3Vl1R>c`EZuo!nZ`FH@5#4_J^mb4_4Hvk(c$}ecx7ZiRR8pTsTnl>uy+h7N=KyX(l zhdSA~S8%Mfg_Mao0uB!Rn2zom-gsAF)zKqAuk%Mayqi$I*EZ$jUA$`K z+1=?daAxFDn}RF(HGwBt^|rgd9`C6yPT(8&94JLj@3zjR2O6|^KcE{rN=~~xj^~Ze zkGk5=(j9qL!S1#3>Ge1E>nIc~S4GTOWfy_$1v|P!2cYeFG*dG_Ly$+2+xLdIkKqqg zo{-ASki3E4ICdB9<7HNoo2bVwCN{bQw!LfrSz0^g5?m+_>(6hq&v>nUWyJRAMq z;6Go@drGYlv}q7Pd}1sY%%kD52sZ14GwE;TBUMA>`dT-{$qHY7jO(`yAS2itP}P|6 z&jbsN#qCzX;`z+Rs`kDz9r0~>u{oggW8)RX<%ssj=S}$N3oI#|_#$k426F(jSxK{6 z<@lU~`i+y&y{ zrg-M@^RJmL&iL%GdK0L6SaCj8NTDydUj;(Sjrs>*Cd`a-tppLBiM)ww@XYb+CGFCi z>@&xn&FxwRAhOO&blKsF*7c8X+P_c`>)bz8eqi%Ib`Sg*gf9Sgx1$&5)6e^iu0c7$ z6UTOnI9X>M?4-{c%B>TdP3SCCR{&rZr8TZE-><^`=rHGAJ5-AAma&Qf88TV~9Ai%` z>$&d&r)R6PU=|FBxonI88PKg*BsZkHPJl`|g$JMdPMx1OZ3ku#_4#bz4kdd#0z1>` zuBopRrF@V)W4`Fp)srg@g~N}SHZdx0l@|-6;kD!Tt^f`})2=tSA<;wey8PbS8TA91 z*=vtAvr36u=Gzl5S*wH{ll>vnnqHfz3lt^%=tu|f(bI_XMpFKia-sijCipMEQLOOK zGY!NG|3-je){Pi0fzhBrMg#f?wh{N}ju&{&L`ES0UeGR=68M^^H*)Z>sV=kA8<97N(|Wz)lVfI0n2;`n)XL_@x{^h?SG z_tUOC0aOLXv}Ye^*3TqF^M`bD?w;n16|{*)RKF6lt-Ige59}Zf;Bd_?-mlSJSOlg5 zn5>a)JVFpGJ-6v5edEbtp`z5K@o225!_y)|SiDuzs-@T4Xavp)E^5^CDAj3iV^qhg zHlG%PsuNAQF*Q}$?X7Gk#KOmKcV~;!ZY-59CnHaRg%wxfkvDOAGJ%i1uBD1KN8GjM zfCyV^nk~p&*OuSctN_wQOhOIhh3Pdkkl+VQinrZa?G|XtQA`EwOI67X1Y>=zpad&@7vM z#2!zIdU)R=fq`mS;0=1`rCbXU?ls%abol@njN#riw&N0u0pVP0DQW-xM2Oq>DEx?b z=2OBxcJ*x zes36A&h0=mW@F?@u><%q!$~rMht!r*w*s{bRqsA-tlqPtEs& z99E;D@GKLH*~b@l)p36BUe3eHYgqQP0bLk?91H-U&E$mVeur={Su63_t?xSKtNxR6 z``Pk8s4_vYgkKl=tz4zO*{B&^;fB%G4WEBc=XT?mX5z$mkq`28k*T#EQ6UwTE?5D{ z?CqXNh!%h5$bj1BcL@;KcMIu0Fn(TOxKkjyiid_z#6EExgwe|6oYoV2(a2@o-8G4w zJc;j}CA^Z02HkHOryNL9)07%)>iV?2Exx-40r&#vhaFNvuA(%9xZbbsMa<96z-s&S zl<&=1_B+-~9nil3Cm*^zi>grLpp4GV#_W0MZ`NH}3_k^*5pc6A_x|`c{2O_W{YQAh zE5N%BNDgY4mClxp>Uq}I0R1VW{RiM##|Iv&4l9#uP10Yx18%OT$gBV*?S%W zbLh7p`s?whUn*Sg0CZJtF6ZP<(La3N5?VyVCO-A$${Vz&MGQQF&>O6dTd3ZZ)kXQ{ zo|!O_f+^a)9SYm}ybG^Ux{JcKvC}H{fg|d3fyw(;M7hWTPWDO#RN;R^w09nDcSVaV2Zr48PW>(|J zM0MZs#KukBJ_P5C0!?*&Jlw2iNu7z)muhyOmAgr?$a}CG-RwjDxn3&|ork8c-sE^ljVs<_)9JU^0x&{7 z7z1OO$u}q09DQ;<=lR`Cvr3JqpFuB3S~gaa-nw;!Pj}Owh%ytZ!us)vQK>|t6FXvd zE)V9K4clqkIp;`JPuc}6Ni7PqaNzs2`BXQ(Z?xP%XMmr5w3PL8a;pLVj4*4)>V7eM z)A#mw_uc6Z)vv^>n$s*Yd*PvW&Qx;3X6k;J+@9yqSfz7*)f;1v!bC`RFw^Pxx@v*j z%+zPGG#>(19Jl1hE@t-P-j2;xzmrn$^2RCJ2`yK(0(m{|dvf@wUB-x3$A+Rn z*Bf>pdh9lh`lXu#oWUuD5wXC{ZJ#J=`2HrV;WE4*q`vV!Rm-c>ZI{=Ffzi`T{VQW#Cc?DyJ$9}`ozdy!*P`v|NM!`gYeB1hLF1HZXDd9BfN z?Xu3G`*dq>fyd_uMAE9;iY=pZ=_6z8Rj;R1;-Lk;LvOBqPFXeq<)o#1&*yTRPM(uv zR+|DMy?;d3Y&KfU?V{M6oss#m+T6if>m;5Ju;uFb{oEPQ z%c5f2j|zp($IfaqvwiX<`ObUgSyms{qVDe9HIjoCOE%}h$ z@=;X=`(jJHp!Wb{&J0JF*ZUSk%Gnaohudj4_Fqp8 z|5QCb^`S(bGv_bhXBWG-7HZ&PU%W^>YrOGY(6LOe9?5>7=PY~L9I#$x@-B@%YG2@I z7d)Qq2H-%DN{9LI=nnA{j6O)LPLaoseJAn4uLYNkvV9&m-a~8yKN0e}RLIzvtnYQ; zh)2$~g8fC^(B1dwOPRtyuhIs@!YYfFqKul0R}H9Ri)P%){cCkBrpA((B>i-uk`=Lr!z=Bs*)A(+n9v%Rw6dii-89BOjA z=lEj0PW>CG_41RI8^vr3A>sbTnm+@(9|(&Ub0{rN!^wW%rrp)NTVATKIL&+ylF_?# z=0CZ{R!C1v9q)VwERV@0N>-x0DBXcQggy zx@~K+pHhepKV+7^L5rkZtJU8h;e8#>qGr@tsQP>q4#mdKWA#g6qFi1e#n9WvjSIb| z_2sT|cn)<-7+_DIrq;l{Ni`49PT>Nh!VNL&a}hFx7FEbN_pgB#++Nf$H2UiJY|inL z++ULKvocrs!F%tU$2Es{1W3bYf3kmJUH)b&J*4MP2?ua(M>>gC&=Ka z?zhNK@ZeEj?Dd#<*$Qs0+4pK<#WQuu$mrCon7w6;~Cf-T)d(G+X2};F92P$19gLui13G zxoxNp8iHj>5>(2g`0^Hr6U7%drKXCuqDy=KP$sP)8<*dqN;b2gEdM6_rGE7_fi2fo z{N<6R4SM-s0AeM~iD?meO>O8Y_BIP|>TfkaNp3t3TE#f~Pi~mAx`#;Q(jtmN* z-XyS2JRgrdUH*oTbyn!C(l9oJc;yqz5<(*|1#T+3U|tk52iCwlJ9>k5nfwN7P0Ic= zX2%@_vcUw*8y>I?9!Ev+#PGDEdZQD#a))om8(a#nSNJ|PmcrEqfXnK9y8BLyB97P* zEe_mcBR6DpHvQoEBe>Wt+y;AK?-7ST%dp~kWxXgXTfj+M}ZVj7rd%I+=RT@Rl#H(xEm6aeM+a-4T7fUI7eqYNe-nD3|D2P4HnZ8D=fzCVIEa}z!ynQ@hH*Xwuta&tJ|z<{~A z=j2REC~#-lSmg-0Z=B#G>zgi|UeD67ZTW3FuQ%XNiEW;)Ijauh%6STK)E@aZ{SQe5 z2WB?%G&3cFUgO%{+jXtWy>}5w3;aMYLER}0IvmML3D8TE_wSAz>lbeA{;q}qkKW7k zkXqw5E`1i3-eU%1u?Jw5gJKfotjI)z^)oBpI+F?T?@9?9afL>w`}?EG=&Q<9R&pj^l@uTO zU68`Jo@}(?9Knlci+gV$Fp$2K*p)$ip9!mWZY>Nc%vj+G{~gDiQ|r?8>J>C~cn>#O z5bgOf;#b+T(Rww8OnNwD-COMp(j7>C0Pg6-BEUDL%{!^m@m6+w0_b(*p7Ozh8JNMZ zWwmanpK=K#IKU_k;9+&-8%<`sqP1|jVaDg$65sAD`_t{G8Q?vq3}hc-`R0_az^Fm% z^M12lgQX53jEDQ#)Vdec(o9^RiT0I?y7ap<8iB)}p~{S~$J}$)4qDi&B9AXHRs1dH zHDzEg>@S$8R{7$J-yCi1YmY1h&Ba_RMI@9Q*Njb}ehbtUBN!nc?$^gnp*4kCaI#0> z4rSxvKG54}FfW`u7dy;5;=A@k-lD`7OJ>;Z?+bcxJa!>wB=5H1{^o>0yeM{i?dXoB zxv=y5%r5btzR&_pb7b5Q4o%yeO{DAON5aK&RLkNFDE36D={F;?%QRnTy;&n_AN7co zpd?-?eCI6j)a$BDH}@-sGKkO&YPo0Ec!1RwBBaB_n<}R0f3ZC2!Khu>I1Z{HbA`AdY_W8XoRgr8y0pI;BFMRI8v*G zZsm8{e(H&9b~>s#IsT=dK|hRM@<*DZCPMi$jjA`w80yup!2=ueSwE`{kaZQ3 z9yZ_>uGDouP<*z!$8qK}AP^Tc?#zy)IROBe^y1&F6)+~l(%xM@Oc9tcK>z98`616z zuS{M`_35Wk+t*(wH7Ez`fConMn*c4j02m*;tXM8=p1u2V$g1NzM5lpOCnx)^Z!quL zVW1g3S1HlCTU`8phZ=6-_xmDie$Qg#SF?i^Xz#k})qppCA#B8hWzFV61|S0zadMpM{w)*tU?U1 z;emd_(#n496OqJs3D7znB23R5Fhqs3NICUxO0^Z6V-Z9~Ztgn2&_Es>?3&K`)_JiN zSF9h;fR0U^s_8-~!Lz-T6jKsL8HC z)z=UX$6N_LY2&6@{^KY7U!>HU{6Dc1@3h;IGyd_vWs@K%->US#AEP!~8yf)hZQaFY zbx(l8%#XmWSFu^FnFtTsj~mdYUYs$S{usbVOD0MHA6B(Hf_R6mRt7SE-`J~8h7({r zo6f~}!md6w2AO_mZ`~|WVXLE`Cp?4_-(}KbpYE$VUa!k($sGF&ZaV~=3Q)LLsTHAJ zC+UFu`@35jqfVJi=9im&zn7mX6rQhN@D->rLPdCPfg9)A%_%mnjEl?pq}A`Y#DS#* zn?B{thilF$qDvJ!=*y)2GK;{V?eN&v*yQB)E0n_eqBXir=iwK)E~cYnt2Ipx{Zm}q zt=FOc9R5xqzua9TVof>$g(x(y^)5~U;QI>!NrjhFQiD*oz&#Oo`Ys|76-fDXnw#4& z$i%kxNWf);|M7G5GJ`;KvQAxYV6Cn5s`j8T21`q)R%4XpMb+35W|Z1|Woy+V>ovc2 zedgXCjm%eCNPqo1VKBp5Vd@9kgna?O8%0f^n+%;%yY~j|t>+S691g9Kg*?KnjOF}7 z!-NhYI}T!#l?g{{${e>f$a`dmPvfp-KT~;8+|0teSnz0eI{Dr)di!*VOiUqFuSd#@ z$DdPW)L+HZRR?}1++%bE)dG!1b*?kV?UUIiY2SJL@hSZm;t|LGC+@8JKh|l$AdhA} zcB52_fbP%*9EXMhrs6N(ydd*A$TQE;7@UK|mfNuGntFI`rm%liBJfp#(9YN?GU7!aUJ$XI&c*K3?h)SM<|HLswJeZ4%5bgv|XiWtOZ|I^rE)zeJWU2UrN&vu(35$W7v^ov!}EdCL2M3 z`}6T6`%w5*HR-1xYa@S90JjUbd_Hx(BXCgS@5T^G2a}DrJSgPz0Vlv#n_aZR2-X)& zL~VPp>$#s(N8yf@B`}}+ z$k6QVaZC2=Y`Fkh_OMm)ncq#&KNGG@wwReq)jw?Rt$bPtue*WKsU{t^7v<~12&3K; zmVF4<#$Tm5T1#xb^w{95&nV((!j^ zEL*kSm!7yK_>4gnqkaJ`L-4FhG4}X%JS>Fg;u)yM4qS@#~%8-ElZLZgu(h0YyRgSbtmiY^Lm^6_AD zQWYd`0N-nz8xe9^h>k} zh@lPv=iKlP7J#>0;_t+%psNVK7Wa5e$-U9!3JqNCxudh+s7JmgNSC_*>BMXN3ypzm z{L?=2pO(1A&sjV5Rc+KS0U^dHgpl4lzdCC`0W0qSfG=w&_35HNQ8|BZvu=r;^uPXa zGE@^S&b)qodA+((ZZM}K;KEYVkwAzmI7x67s&!|-^ZR_(=fh7vl^-Ju3ag1*Tz{cK z!^5htcjpE=5Z5E4>FkbMqr1M`obM5E*<=G0E9jVHHbV8HYz1D_vmCmOTrGLAeNf|9 zeEQ2{p>N#`x2VKzdhN`F;392<()lumo*?c)7vcE3UOs0FFj(tP9^bmgzBr_M7y7u4 zUNag+kPT2sboEpWryLvAP@^bDB}f#A{en2JD=ol^0`#L^SAe6>>3Vg9^}BkrZ5I6C0LV zfMZ#mcara_n5=0N6U^1T>=azTCqk2q6YzlrpKk!l4Ei%m^QlQw*GcW9EzP$E46e#> zH-@!#4$s=%PSK~v95ioS;bi^7kJI;RFPG;&BCf^724(Wu#&SbVDn{dx`z3A_=PE(; zxv-c8(swwml~${{TjYN1<6UjcmSW{cW#riEImR}r*4EhYiOZvTnH{C&=CLyxKy0(W zV8_O&7)(cs#W-kE{7isbQJmC16glpG`u<@CI8l>f?*M8vN~KwHpl6a%X!5fC=#8F& zPA+Zd{WYpGAm;>?A=S=j;;i!g<9qWjLRR(v7~p`xO{h$6FC?e#Gq*QWY_#)XE9%^A z`})~gQJcah_%;0%pvmwW3T}D*TjWx!?(>AMVd&7RHnuAe zIHzaiZEsz_J+Jeo^k>p6{l-V)$@}GPe*v#gA=t`7*3h`r*%EY$h37E~T)2*H=+#Wv z1KRKtwB+#5^uUcqtHhXc{nkX@Rwi)huD(6qblsfcb#@5$x#RG(oh_-Dmnc!j@FU@S zW#FF}Ehpx$PUB=PdV#GJiHsK5lgo-590jq^urv(+=xb~Ii(Jls9!!5>^C!FO9u*C7 zx$Ft5jsoCuRC)@{lXUm1@2OK&TsL@!HPadAbr9ZdO(v}c4wRzL=6qGGdL4a};UfXD zk!|)0#paYbK6c8y3QEpoevY?0_@Ke;-eC#0n-II>e+oUIHC7b>rh8Mv-*&4mT^ zepMe-l`&(&a%RDZynWTsvFK9Q($77)S1 zg|t#%7>_#wxTU`*4^NKT1ypD*%U~-;!D_+9 z=;NM>$5V=f^h38Ue;*xtiTcWm+R13a_6UwIOsM# zNWf(0Q$2rnI_yg55gB~g^T5(xF3fcg_1(`-L2t+CqQyjZi6z{3Y>Cap9DO zQ|eK}K$IVFT}InJ&NIZ`AWX3Ldwn|B`~)u7S|4_RW*mWCqXmjqZ-XgkLc3NgiQU*y z10L+r0NDQqR(rSNpAV*gAz?Iye~vJ~|Mvg$gZ7IjZu${V74Z4CSP`+E82}fPj$?|YqL#ytTak1^)IQP6c7Co7HVf~{&K7gI6mwo}0?a|wAjM_&~ zcMWolploiqJ~6{p1ysWZJk(*5##|4SEBKUU0623ytk!nNAj?*^YXjc%R+_V4e_}%T zHT^baX5G{0i16{<(az5*Q`tW3&|-7s-v+}@Rap!hY&*>t3mexK=eIQ=-&bnwPgACc z22&+GHi75Ox@SI!f@^9CEv2Xwucb!@Uuw%UgZ0jWjN8eG3VPqt&JRE4i9Djf?}C1> zwM4&jg+=RIv}VOaeQV;|%{mN>54mZnYRRe$^{8>V1PJm1%>kz=Ujl^11JxT(th^RL zwQ*xn>Tj{ikO)t-7pU9jy*APu!%w0OF z#uPod6zw{wjYbu`!lR|3dwgNiG<3Hcz1>}4R ze0!#Mu7iy6+aW3nWxqXV_v5r7C^G{Ef9>yaQoaE`TJ1N|Pn(1-! zqrrm$iTddR6R%a7*_2aWvB%X@?SW4u8uQehtr{^(-Qe)Ekcvm3lse<_8P3tIkEr+K}W8ii+sT zfyhhljACpho^usCQD&ngWZ)#8J;19AD)Mz$u zG4IFYLsn|^rKg-eKS@zTa;&x^hOq|k9ZG*M$1_PL))iZ6?bvZL2M&D#Adj>BfC?L}lKa91 zS~pvX-m?!&I0>Yr37X65J^1q_>&UROc@zWSi#j{qW4lMYRV-cK^EkZVE5GzgD_H&G zC=oz?-iL27OP%EslpWEX*v`T>{% zsf`YY`QscFdgL(>!nKvOyy}IiPU5Fn6kSqpFOT^aD&5-HS57vAMt7b+6l8sHu7qk!D`qh+K&?MAlm$8V(@%%y5r8o@*K(^TtO&?#$o z01fNR3I5d@gk7RqoU`=S+foFWq_w@+yMpYrl652S^C#uU^!{9YWksWh4YVP}=k28w zDPkr`zu_|Hn8kgzf1GdP9wKx(*^8QVOXI{TD$XPWY$M?63X8b7ZXkqH-Ct zm5ksfWeKe!k)~=tDATTW;OBGdWU(9UDbinn20DX8yes#3RvP72|L}s3BVsj-ICux& zU#+1g?&#&G@Ec?M(^(IwF=Vd~N411+q#ijK%I&4n3Pu3w^5`Hj=-g@9`CXJduk0O4 zPx_+WXu@KZ4ZjAe*x}F>Nx=Y|0gZ3mv(n zh`!oW^)))00?TEs00N~X&%3Z2uCj$Wa=578aVWzLgg##l0|c=*e;KM z2B_~FtRhcjSz1DRdC_`UQVeP+deyt4$?{f{*LgKwn@z`&0%Mpf zhv_0}{+{ZPtt?}ulh0mo@6ZTAC=bh-#;Su70Q1UoVsI$}K=dK(F_jVl7Kci}CuTRx zL{o8kZs8jrdp&pAF)H`d>R2~6Us%@zB}eCoyOns zW=AtV?)&W(-57)Nsg)g&zxVAL%@wwtpU=zfShjMLzO0UhnI@&OgHmKEUl>60o-J5&$X%FyE z;c@0H;PP1F*X@02YU?bD)xyfE)hXB&;Ac-669p$CNJk!SO+YX`6&ZE+5Vj3+kR7H# z@3YSc@mZeAT?jYNSTlr+C@7Br_{+h)DyU;dCKLH0>`IkLB+BR-EQHF`rPV`+`@z$j zE9Sj6{Zw#{P}5~mGtxi4djCRlqyGbl_r?m1_O3iYauIc-u%5P~CpNRH-YRNe>`l|- ztR8bH4oe>wc<;3m2&i{&cs|wDv@Vw0?p6MlbXh_g&;TGdTZh@ez_g=SY2Wl>mj6D< zZ2xL5FIE)M_P~2gN8x1;@D0O3lmh=HBJanksVcojwjJyuol8EtNTC>EQc@hXmk0jt z?cRod*mJDraLl31P!!)*Rf>d&tYiN1xASkLp8vRZ{>Ds4elv%^1YKB)j0S0tTz&`TGR_=aaG(( zpR&`~Y+6lrb{0Tw>QQZO2Z5Kbqy5PoEt3 ziM(2}Ihr^^Dew`^@s(~zqgO0P?#~tDUR#gYa5yr9n$VjR(=g98^0-G-x-(rhsrRtW zY+Lty2&6j@HJ`j^9bi`yzx@pk;zORV%?)KvZ z9w}IeD;l6$4(Te=RGiQJT87V~$yy{wm*evHQb>acvMZneQzd z$3vxjDnj@&J6zm6T>Xu?)$0>=yck#9LmIJ%-tu-v{Zb)BW1GIy=0TnNj>}|knO(JI zZtOz(h$DQ*G9X>3bdMPDHLG=}t~~Z#+41YdTo}hX82LC1 z+viETThU)CTCDPXEuqeq#(H9`{0`uy&Qsi|wOoB6BBI|J7&vxRs_DzZNVQg@MI20@ zo{O_e?mmBwZj;;t?+7GUD^BA~&o9z$hIR6~R=XCdvTj|)tDU`1=h6)2+&YjGZzC2Omqcl*?^;2i*G#1k!j@N=ocFC7I{$7}`rUL-~ zZ8jyJB4cB6bkEipZNwOjK1C$gdT7bh`*yv~XY^zqh*ZB=Ngt}kvwUq`PI{v*gcR_@vLCZL z!rqzFr4_SktDN#^d_)BUF%W4Mmgw7iA#?{pC%Kpe1aRfkqI0#Waq}3=hhaTetK3OS z8}RIBEEr-A#2CtM#=l7XXw4?DYgjtGWW0OI^B#50X~;&r=}I5Jsqtcl-*)Km-8&FT z+oUzMp9U_~(UEO(Ky5X5yrBY0(y8Tvw>NWHtc;fF^( zfstY_w#p;Q*|hf_ah=L_DrsLZcaHVAULj#RsToKB!s_#JGw0-C`&>Csu1@xMVP~@( zSRn|)10%hBIQz>!$DHZW6H?NYro};_RSf`70-}BA#Bh8%ohrbHuRVGxU$-z8^H)K| zcuvGI*3Hlc;1u3-tu>X}i(>sX+7wz+aep~};JnGDZ2Ym9!Te3N=u5-unMxFaCdCDk zu`HLeAD>Y%tF3W=Un^~$*2P-rjCT|Za;}!Stwa5&yay{>Tp-eI?dQtrPDeLBw)1d$ zP9pqMTc`2MZZ-gGZ1 zpe$Pp`*={fQ{$|m$U9k}grk0txUGC20H?9Eem~JqufJO??!hBLdFzIZz8Z5E+!Kmw>LIIRBo@_eso##JeylLl6v}`@bYxE%g394jD3GmClZL? zfn_gB|4$x=A@vW^i4$}QTVOJXD%CL|`5A4~g4|kNHo}nYES2Gvez8bcPty7z2Vf=3 zK7VX8L*o6ce9W7#pcE^jbOQC0gH0iSf8E#9i2$sn=R)r=A1QUcw<8ws&1t7mlU}>g zZwS>Jg<|!rPbNWK1z={}?vQB%jZ>uk=1StvR7CzqTccjr1r0kPvfbHnYIS=iNIiR8KG zFREdiut{a;WAkLK@po$%{ zC>*-oWP9L@;Gk742?g$H%UkG~A`OM0iz;5T1^J+j%wXbC9zQP{&snuJ|C};BXq*%< z<0T(_LOECKaqLvLHD|{;DZIL$&UPrUh*3%CKEnz=n)`>3{;&XIQz& zVrn?c0~o!!w_2Fnysu-mMV}A}33aqV56)I*U2^k5@x}ygwQ6tW1X^gC>@I1yt|t#E zq81ozkpV4`f@h8tqvew?Zc7zo!*sjf&K&$cn$7K$6dz2z?Qu&?c>SB(7Q2>hFh6Z!iV&x>6CX9NF2 zd`Q_v(G4aD!QgKVwAe$GkMr~S7BfTz2#cS1VR!zcrNfyhr zt+3?U`bNk}RswPE>Xk*}*Xyl;KPMHSIH?v#WQimkqmlcZyB(iSDl9+IXQGm18RL2& zrk^R>msvj5YG$qW)80MO=KN89RF+4LP-@fxw2VVs^V!%$DoOhDRXr=9Q&eA`M(@Qy zT|F%J@F00DVNw&Nl8hy72Axfv_TqFynxz+IW`dV@UJhj26hyv}>sEQVU`6Z01prrS z67AXi=(SGuSH3GceX%(0P7_M0hfOuS>!MhLm{&pmVvxK!nG`6=8@-@7tudGJlP>bg zs0dn8Lv*QvZzLDV6`Q$WYFLtxWdQjsIvz?+yBdvM)`dlHSb$97PYLqDhq##Tr zJpq^1e(lgh9t)(RikTfY8MtKy!)AWiyep7bVL`i+ZJ6?h5#?2X)0-%(GB#XsD|;l* zYyECpn8uv0=QJ>_RBfyaeQ&c_*ZOCDj87#4WMOJ?n>1pBKf?EzzbG^S!SQc}hR;@3 z7nJ7zIpV#-=oXuUks+=4^_)zPB(a?B-R@V-c)Ri1B3R91N;Q^G z0MUcd+F`G)&=Q-lX~6F=0U^1@-R3M%tD#?}HtF2b!_^w?CCrJ}Yg7wm5LBH?V$I!R z6I(rN;*h$Z&tgTCCtn!G@TW;JN?(_8I-Ha#woHi#{XuD+q7@;-7X2;r!dzGfMSM2Cd zJvA@N*Cw9fFWZo&I%t!Xku|}Dl4w4+{l4CKWP8`M-r&ebyxF#n^j3~r{HdD?TZh!9 zC+70Us)UCOTA)vpb<0T((iXzYtb5G0HmzDIA#I#|iHeU&IBhk3EgYu^pe%t84g5kl z*ipWTYAK7MO!b~mEXtP*b4l*SnOm3L^o}LE^rp#d_slA3gj zDYNB9H3pkzylz<&4YeI6vs{KUy9->tC-Ulc1i=J#!lcbNDu9l&`lajrg)wQH|C1HT>|-UHVTyC|~fYR3u+ z>&aBT{7{^e4J_&&J%(dt&qa+~wn(vX+?el*n}XL>udn!gk9}e45kN3yB;B}o*ZU2U z0#0^qdof?-jH@*~LMR+@6%ao$A6TNR+9L!+#)l6Ec~_f>-lV#yO+d&4;~$|!UC--G zI(xy!AG_-|+)JU-&-c|pZ=ycko~%tjR^GtWw@jIMAC|sIdmn281R|;A?1ReJSYf-Q zt?XL(Y#pgTpEA{})NxDG){>gtHD*0^EC<}$J8p;|Svw*bbz_y`FIG}gjg8oOz5B(t zeU)U4uv0!MOZm&8KWSF4)q6jvOfw~_7x#y#h-Z;$rzt#EXWvOy@5PqZ=5%kV?{4i? zF?Uf&4V#f?`?07i&BBI%`v<-Vr@v;YODVAk#qHx6UImU*J|xJiZ;N~VCRaMCvNU1^ z1_ag5uWq%At6l?%A!N-nK+e}jFs;tF&XYk-i@j z32#-EW5STM4-aZV{UB|0_P&lRwl7}6Hr=|^^36l@8Qa}2D^#N6MAjqD#`NS$_U4rl zr}VNAo`IbMZ5-_ffTIo^8XkE#N%Z`)i~mJY09Itb$8j?EfAAoX_UdD(xRS)@DWr&L zDoHE_{0_RNuN-uFUEQQDwz*k!wwB*ZuHUBLE&`GN*32Omms%ix6Hn2X^E_6zeN|nd zgk6d?l59SG+Vs4#zBbts->xcsZVXB=laQ*PFJT|lkK|beGLx%y9_G5EgPDJ4&S>CR zh1@d$4E%|yrs-q0R+{&k>$_$$#gIEntQ+S*Jh_{`)ZwW0M(V=Wd|+2OY1&{;c{!2w zeTN8~5KAQSB%dLGnKQ{02E3=@#<=rnC3Y^QQ2Av0V<$1-Xk^L)&dT?$1S?rqbXZld zqXB!TE!*_sJdswPFNX=12ex-m`z!J)#g@_tbQi;`e9Uw^hpI&kipyp$hazn(J7Up$ z#!L3OTmm%x{4J06&)LJQvz$(dsW2<5@pI=+tLx!z6`(=9uZbUe(zWj1_+8I-QiH_Q z$vy%^y8-c0X`bz@2ePfl`IWYPo%^4jAAa6Ix$4NtPp+rHq%Wm17WOrJJ4M zPVIW7bpE6kVK;N|rY~la%`M&{pC7T8`*@g`p|dJ81wx{-ubygFyO_weiZ5J>>0Be8J6SX!T9qkXI)Mxsua z*5Pb2rw9S~0a&F^=*cObac6LV9owfm?^0&st|+(AD{YR|0*XDw*&k<-zo;yc!tX;2 za*iE$P5%;cS9dT4n4fa5bSPEG-sp|B(CQV|KjXWr?7~GYqhaP?e-N_eZ9Iml<%GQ( zuny0zdJP6WOFG}g1!CTqPdUScRgMHCd zI}g=>s}Ih~$7#Q;<}NFY4N zhHdbbN3G4xK(_dBs(MT0(e&CNmUqZK4K;v`onY)7C zp`SzOT34W;JMgU$;IOg;ye0u&5;wQ-xKZt*SesVz7#QyYu{`(padjP*a zKf`}tBNQmZBQH$X@e4oNE~kV2#lh87}iFsu-1gW-jUA26a2X_qC1$U9=0NkVu!$U5jzAiItE3Oi`XI3T*MBM7JR7k?6xnY(NP=X)?H~!7 z{k8)%quFmeNP=g-?H~z}{q`%8B-w8}NRp1kA-IkGwgb48nQuEtK*mJuphzZS2SrC> z7)i0ewo!ibU6SG>aT3l-G7&pyl8M+s)4vK!zyBW1{;CFlT?59I$Tg9U*uju=#14k0 zBkzOx75%^3V4^XMWQ0iEAXz5bryMxK_wR8+^j-2)^t-$m8JDCW5RtwTiAX$v$Qg-E z6am5b>scU*M|_e3ANuYW3O?WOgQ9@F;QRMzHqxiRriI9Tj39p7L9vwZ{odd_S;TiK zj*H&M6XN%np?H>xTmzl;Z~vnNE)ruYk^RME@|&;GfSUOJJ)mF`zx$M?$jF=wHIm4< zqygvm?UOXfS$sbqjA$bAEG!op3pB?>;s!+LNX|fmc+Ph}(*hHjqi8Ye2Qd3azRM77 z#3vb&iufNxQIRptz-`}s%0SxmJ!Tjt8iyE$jo1q#FcNnemXG)g!?Dq4;iGvIBSgm* z1AP^FK9P>(LyX8p<{6e?B4Y;lOe1qLOA*9(zko+Z=6Hx!kvSAni%7q)3>V2uU{;8{ zbC~fWxgHCz=;p}W%|goh?a!{rec?2;sl`;Qid5 zuHN&9EdMC9w4XngvcNH=lfsF5ssuu;Rg!1*T#?k E1^5Y}rvLx| literal 0 HcmV?d00001 diff --git a/create_handson_workshop.py b/create_handson_workshop.py new file mode 100644 index 0000000..8dda693 --- /dev/null +++ b/create_handson_workshop.py @@ -0,0 +1,899 @@ +#!/usr/bin/env python3 +""" +Generate 3-hour AI-First Development hands-on workshop presentation. +Attendees build from scratch using unveiling-claude as reference. +Tool-agnostic with Claude Code/Windsurf as examples. +""" + +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import Spacer, PageBreak +from reportlab.lib.units import inch +from presentation_utils import ( + create_document, + create_title_slide, + create_section_slide, + create_content_slide, + create_two_column_slide, + create_code_slide, + create_hands_on_slide, + create_thank_you_slide, + export_prompts_to_markdown, + NumberedCanvas, + DARK_BLUE +) + +# Collect all prompts for export +ALL_PROMPTS = [] + + +def create_presentation(): + """Generate the complete workshop presentation.""" + doc = create_document("ai-first-workshop.pdf") + styles = getSampleStyleSheet() + story = [] + page_num = 1 + + # ================= + # TITLE + # ================= + create_title_slide( + story, styles, + "AI-First Development", + "Hands-On Workshop", + "Build from scratch using AI • 3 Hours • Reference: github.com/emmanuelandre/unveiling-claude" + ) + page_num += 1 + + # ================= + # AGENDA + # ================= + create_content_slide(story, styles, "Workshop Agenda (3 Hours)", [ + "Part 1: Foundation (20 min) - Philosophy & Setup", + "Part 2: Project Setup (30 min) - Create Your Project", + "☕ Break 1 (5 min)", + "Part 3: Core Workflow (60 min) - Build a Feature End-to-End", + "☕ Break 2 (10 min)", + "Part 4: Testing Deep Dive (25 min) - E2E & Unit Tests", + "Part 5: Git & Best Practices (20 min) - Commits, PRs, Prompts", + "Part 6: Final Challenge (25 min) - Complete Feature Solo", + "Wrap-up (5 min)" + ]) + page_num += 1 + + # ================= + # PART 1: FOUNDATION + # ================= + create_section_slide(story, styles, "Part 1: Foundation") + page_num += 1 + + create_content_slide(story, styles, "The AI-First Philosophy", [ + "Traditional: Human writes code → AI assists", + "AI-First: AI executes 100% → Human validates 100%", + ("This means:", [ + "AI handles ALL coding, testing, documentation", + "Human handles ALL validation, review, decisions", + "Clear handoff points at each step" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "The 10-Step Development Process", [ + "1. Specification (Human writes detailed spec)", + "2. Database Schema (AI designs → Human reviews)", + "3. Repository Layer (AI implements → Human reviews)", + "4. API Endpoints (AI creates → Human reviews)", + "5. API E2E Tests (AI writes → Human verifies)", + "6. Frontend Components (AI builds → Human reviews)", + "7. UI E2E Tests (AI creates → Human verifies)", + "8. Documentation (AI updates → Human reviews)", + "9. Code Review (Human conducts)", + "10. Deployment (AI executes → Human verifies)" + ]) + page_num += 1 + + create_content_slide(story, styles, "Workshop Approach", [ + ("What You'll Build:", [ + "Your OWN project from scratch", + "A complete feature with API and tests", + "Real Git workflow with commits and PR" + ]), + ("Reference Material:", [ + "github.com/emmanuelandre/unveiling-claude", + "Contains working examples to study", + "Use as patterns, not copy-paste" + ]), + ("Choose Your Stack:", [ + "Go, Node, Python - whatever you prefer", + "Prompts are stack-agnostic" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Prerequisites Check", [ + "✓ Git 2.x or higher installed", + "✓ Code editor (VS Code, Cursor, etc.)", + "✓ AI coding assistant access (Claude Code, Windsurf, etc.)", + "✓ GitHub account", + "✓ Your preferred language runtime (Go, Node, Python)", + "✓ GitHub CLI (gh) - optional but recommended" + ]) + page_num += 1 + + # ================= + # PART 2: PROJECT SETUP + # ================= + create_section_slide(story, styles, "Part 2: Project Setup") + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 1: Create Your Repository", +"""Create a new GitHub repository for your project: + +1. Go to github.com and create a new PRIVATE repository + Name: my-ai-project (or your preferred name) + +2. Clone it locally: + git clone https://github.com/[YOUR-USERNAME]/my-ai-project.git + cd my-ai-project + +3. Verify: + git status + Should show: "On branch main, nothing to commit" + +Reference: Check unveiling-claude repo structure for ideas""", + "Local repository initialized and connected to GitHub", + page_num, ALL_PROMPTS) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 2: Create Project Rules File", +"""Ask your AI assistant: + +Help me create a project rules file for my project. + +Project details: +- Language: [Go/Node/Python] +- Type: REST API with database +- Database: PostgreSQL (or SQLite for simplicity) +- Testing: E2E with [Cypress/Go test/pytest] + +Include these sections: +1. Project Overview (what this project does) +2. Tech Stack (language, framework, database) +3. Project Structure (recommended directories) +4. Commands (build, test, run) +5. Git Workflow (branch naming, commit format) +6. Testing Requirements (E2E mandatory) + +Save as CLAUDE.md (or .windsurfrules for Windsurf)""", + "Project rules file created with all sections", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "What Goes in a Project Rules File", [ + ("Project Overview:", [ + "Brief description of what this project does" + ]), + ("Tech Stack:", [ + "Language, frameworks, database, testing tools" + ]), + ("Commands:", [ + "How to build, test, run, and lint" + ]), + ("Git Workflow:", [ + "Branch naming conventions", + "Commit message format" + ]), + ("Testing Requirements:", [ + "Coverage expectations", + "What must be tested" + ]) + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 3: Initialize Git Workflow", +"""Ask your AI assistant: + +Set up the git workflow for my project: + +1. Create .gitignore for [Go/Node/Python] project + Include: build outputs, dependencies, IDE files, .env + +2. Create initial directory structure: + - cmd/ or src/ for source code + - tests/ or test/ for tests + - migrations/ for database migrations + - docs/ for documentation + +3. Create initial commit: + git add . + git commit -m "chore: initial project setup" + +4. Create feature branch for our first feature: + git checkout -b feature/user-auth""", + "Git initialized with proper structure and on feature branch", + page_num, ALL_PROMPTS) + page_num += 1 + + # ================= + # BREAK 1 + # ================= + create_section_slide(story, styles, "☕ 5-Minute Break") + page_num += 1 + + # ================= + # PART 3: CORE WORKFLOW + # ================= + create_section_slide(story, styles, "Part 3: Core Workflow - Build a Feature") + page_num += 1 + + create_content_slide(story, styles, "Feature: User Authentication", [ + ("We'll build:", [ + "User registration endpoint", + "User login endpoint", + "JWT token authentication", + "E2E tests for all endpoints" + ]), + ("Following the 10-step process:", [ + "Step 1: Write specification", + "Step 2: Create database schema", + "Step 3-4: Implement repository and API", + "Step 5: Write E2E tests" + ]), + "Each step: AI implements → You review → Approve or request changes" + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Step 1: Write Feature Specification", +"""Ask your AI assistant: + +I need to implement user authentication for my API. + +Requirements: +- Users register with email and password +- Users login with email and password +- JWT tokens for authentication +- Password hashing (never store plain text) + +Database Schema Needed: +- users table: id, email, password_hash, created_at, updated_at + +API Endpoints: +POST /api/auth/register - Register new user + Request: { email, password } + Response: { user_id, email, token } + +POST /api/auth/login - Login existing user + Request: { email, password } + Response: { user_id, email, token } + +Success Criteria: +- Passwords hashed with bcrypt +- JWT expires after 1 hour +- Proper HTTP status codes (201, 200, 400, 401) +- E2E tests cover happy path and errors + +Please confirm you understand before we proceed.""", + "AI confirms understanding, may ask clarifying questions", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Reviewing the Specification", [ + ("Check that AI understood:", [ + "All requirements captured?", + "Endpoints match your expectations?", + "Any clarifying questions to answer?" + ]), + ("Common clarifications:", [ + "Password minimum length?", + "Token expiration time?", + "Error message format?" + ]), + 'If satisfied: "Looks good, please create the database migration"', + 'If changes needed: "Change X to Y because..."' + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Step 2: Create Database Schema", +"""Ask your AI assistant: + +Create the database migration for the users table. + +Requirements: +- Table name: users +- Columns: id (primary key), email (unique), password_hash, created_at, updated_at +- Add index on email column for fast lookups + +For Go: Create migrations/001_create_users.up.sql and .down.sql +For Node: Create migrations/001_create_users.js +For Python: Create migrations/001_create_users.py + +Use appropriate types for your database: +- PostgreSQL: SERIAL, VARCHAR, TIMESTAMP +- SQLite: INTEGER PRIMARY KEY, TEXT + +Include both up (create) and down (drop) migrations.""", + "Migration file(s) created with proper schema", + page_num, ALL_PROMPTS) + page_num += 1 + + create_code_slide(story, styles, "Expected: Database Migration (SQL)", "sql", +"""-- migrations/001_create_users.up.sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_users_email ON users(email); + +-- migrations/001_create_users.down.sql +DROP TABLE IF EXISTS users;""") + page_num += 1 + + create_content_slide(story, styles, "Review Checklist: Database Schema", [ + ("Check before approving:", [ + "Column types appropriate for your database?", + "Primary key defined correctly?", + "Unique constraint on email?", + "Index on frequently queried columns (email)?", + "Timestamps with default values?", + "Down migration is safe (doesn't lose other data)?" + ]), + 'If good: "Schema looks good, please implement the repository layer"', + 'If issues: "Change X to Y..."' + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Step 3: Implement Repository Layer", +"""Ask your AI assistant: + +Implement the repository layer for user operations. + +Create a UserRepository with these methods: +- Create(email, passwordHash) -> User +- FindByEmail(email) -> User or null +- FindByID(id) -> User or null + +Requirements: +- Use prepared statements (prevent SQL injection) +- Handle database errors gracefully +- Return appropriate errors (not found, duplicate, etc.) + +Place in: +- Go: internal/repository/user_repository.go +- Node: src/repositories/userRepository.js +- Python: app/repositories/user_repository.py + +Follow patterns from the project rules file.""", + "Repository file created with all methods", + page_num, ALL_PROMPTS) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Step 4: Implement API Handlers", +"""Ask your AI assistant: + +Implement the API handlers for authentication. + +Create handlers for: +1. POST /api/auth/register + - Validate email format + - Validate password (minimum 8 characters) + - Hash password with bcrypt + - Create user in database + - Generate and return JWT token + - Return 400 for validation errors + - Return 409 for duplicate email + +2. POST /api/auth/login + - Find user by email + - Verify password against hash + - Generate and return JWT token + - Return 401 for invalid credentials + +Include: +- Input validation +- Proper HTTP status codes +- JSON response format: { success: true/false, data/error } +- Wire up routes to main application""", + "Handler files created and routes configured", + page_num, ALL_PROMPTS) + page_num += 1 + + create_code_slide(story, styles, "Expected: API Handler Structure", "go", +"""// Pseudo-code structure (adapt for your language) + +func Register(request) response { + // 1. Parse and validate input + email, password := parseRequest(request) + if !validEmail(email) { + return error(400, "Invalid email") + } + if len(password) < 8 { + return error(400, "Password too short") + } + + // 2. Hash password + hash := bcrypt.Hash(password) + + // 3. Create user + user, err := repo.Create(email, hash) + if err == DuplicateEmail { + return error(409, "Email already exists") + } + + // 4. Generate token + token := jwt.Generate(user.ID) + + return success(201, { user, token }) +}""") + page_num += 1 + + create_content_slide(story, styles, "Review Checklist: API Implementation", [ + ("Security:", [ + "Password hashed before storing?", + "No plain text passwords in logs?", + "SQL injection prevented (prepared statements)?" + ]), + ("Error Handling:", [ + "Appropriate status codes?", + "Clear error messages (no internal details exposed)?", + "All error paths handled?" + ]), + ("Validation:", [ + "Email format validated?", + "Password requirements checked?", + "Required fields validated?" + ]) + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Step 5: Write E2E API Tests", +"""Ask your AI assistant: + +Create E2E tests for the authentication endpoints. + +Test cases needed: +1. Register - Happy path (201) + - New user can register + - Response includes token + +2. Register - Duplicate email (409) + - Cannot register same email twice + +3. Register - Invalid email (400) + - Rejects malformed email + +4. Register - Weak password (400) + - Rejects password < 8 chars + +5. Login - Valid credentials (200) + - Returns token + +6. Login - Invalid password (401) + - Wrong password rejected + +7. Login - Non-existent user (401) + - Unknown email rejected + +Use your testing framework: +- Go: internal/handlers/auth_test.go +- Node: tests/api/auth.test.js (Jest/Vitest) +- Python: tests/test_auth.py (pytest) + +Include setup and teardown for test database.""", + "Test file created with all test cases", + page_num, ALL_PROMPTS) + page_num += 1 + + create_code_slide(story, styles, "Expected: E2E Test Structure", "javascript", +"""// Example test structure (adapt for your framework) + +describe('Auth API', () => { + beforeEach(() => { + // Clear test database + }); + + describe('POST /api/auth/register', () => { + it('registers new user successfully', async () => { + const response = await request(app) + .post('/api/auth/register') + .send({ email: 'test@example.com', password: 'SecurePass123' }); + + expect(response.status).toBe(201); + expect(response.body.token).toBeDefined(); + }); + + it('rejects duplicate email', async () => { + // First registration + await request(app) + .post('/api/auth/register') + .send({ email: 'test@example.com', password: 'SecurePass123' }); + + // Duplicate attempt + const response = await request(app) + .post('/api/auth/register') + .send({ email: 'test@example.com', password: 'SecurePass123' }); + + expect(response.status).toBe(409); + }); + }); +});""") + page_num += 1 + + create_hands_on_slide( + story, styles, + "Run and Verify Tests", +"""Run your E2E tests: + +For Go: + go test -v ./internal/handlers/... + +For Node: + npm test -- --grep "Auth API" + +For Python: + pytest tests/test_auth.py -v + +Expected: All 7 test cases pass (green) + +If tests fail, share the error with your AI assistant: +"Test [name] is failing with this error: +[paste error output] + +Please analyze the failure and suggest a fix." + +Continue until ALL tests pass!""", + "All E2E tests passing (7/7 green)", + page_num, ALL_PROMPTS) + page_num += 1 + + # ================= + # BREAK 2 + # ================= + create_section_slide(story, styles, "☕ 10-Minute Break") + page_num += 1 + + # ================= + # PART 4: TESTING DEEP DIVE + # ================= + create_section_slide(story, styles, "Part 4: Testing Deep Dive") + page_num += 1 + + create_content_slide(story, styles, "Testing Philosophy", [ + "Both E2E and Unit tests are MANDATORY", + ("E2E Tests Cover:", [ + "Complete user journeys", + "API endpoint behavior", + "Integration between components" + ]), + ("Unit Tests Cover:", [ + "Business logic and calculations", + "Utility functions", + "Edge cases and error handling" + ]), + "Takeaway: E2E catches integration issues, Unit catches logic bugs" + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise: Add Unit Tests", +"""Ask your AI assistant: + +Add unit tests for the authentication logic. + +Create unit tests for: +1. Password validation function + - Test: 8+ chars passes + - Test: < 8 chars fails + - Test: Empty string fails + +2. Email validation function + - Test: valid@email.com passes + - Test: invalid-email fails + - Test: Empty string fails + +3. JWT token generation + - Test: Token contains user ID + - Test: Token has correct expiration + +4. Password hashing + - Test: Same password produces different hashes (salt) + - Test: Verify function works + +Place in appropriate test file: +- Go: internal/auth/auth_test.go +- Node: tests/unit/auth.test.js +- Python: tests/unit/test_auth.py""", + "Unit tests created and passing", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Testing Best Practices", [ + ("Use data-test attributes for UI:", [ + 'data-test="login-button" not button.primary' + ]), + ("Test user journeys, not implementation:", [ + '"User can log in" not "Login function returns token"' + ]), + ("Coverage priorities:", [ + "1. Happy path - must work", + "2. Error cases - validation, auth failures", + "3. Edge cases - empty strings, nulls" + ]), + "Run tests in pre-commit hooks" + ]) + page_num += 1 + + # ================= + # PART 5: GIT & BEST PRACTICES + # ================= + create_section_slide(story, styles, "Part 5: Git & Best Practices") + page_num += 1 + + create_content_slide(story, styles, "Git Workflow Standards", [ + ("Branch Naming:", [ + "feature/user-auth", + "fix/login-bug", + "refactor/api-cleanup" + ]), + ("Commit Format:", [ + "feat(auth): add user registration", + "fix(auth): handle duplicate email error", + "test(auth): add E2E tests for login" + ]), + ("Hard Rules:", [ + "Never commit to main directly", + "Never skip pre-commit checks", + "Never commit secrets" + ]) + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise: Create Proper Commits", +"""Ask your AI assistant: + +Help me commit my authentication feature properly. + +My changes include: +- Database migration for users table +- Repository layer +- API handlers +- E2E tests +- Unit tests + +Steps: +1. Review what's changed: git status +2. Stage all changes: git add . +3. Create commit with conventional format + +Suggested commit message: +feat(auth): add user registration and login + +- Add users table migration +- Implement user repository with CRUD +- Create register and login endpoints +- Add E2E tests for all auth endpoints +- Add unit tests for validation logic + +After committing, verify with: git log --oneline -1""", + "Commit created with proper conventional commit message", + page_num, ALL_PROMPTS) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise: Create Pull Request", +"""Ask your AI assistant: + +Help me push my branch and create a pull request. + +Steps: +1. Push branch to remote: + git push -u origin feature/user-auth + +2. Create PR (with GitHub CLI): + gh pr create --title "feat: Add user authentication" \\ + --body "## Summary + - User registration endpoint + - User login endpoint + - JWT token authentication + - E2E tests (7 passing) + - Unit tests for validation + + ## Testing + - All E2E tests pass + - All unit tests pass + - Manual testing completed + + ## Checklist + - [x] Code follows project conventions + - [x] Tests added + - [x] Documentation updated" + +Or create PR through GitHub web interface.""", + "PR created on GitHub with description", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Effective Prompt Engineering", [ + ("Be Specific:", [ + 'Not "add validation" but "validate email format and password min 8 chars"' + ]), + ("Provide Context:", [ + "Reference existing patterns in the codebase", + "Specify where files should be placed" + ]), + ("Include Success Criteria:", [ + '"Tests should pass"', + '"Return proper HTTP status codes"' + ]), + ("Break Down Complex Tasks:", [ + "Number your steps", + "Validate after each step" + ]) + ]) + page_num += 1 + + create_two_column_slide( + story, styles, + "Good vs Bad Prompts", + "❌ Bad", + [ + '"Add search"', + '"Fix the bug"', + '"Make it faster"', + '"Add validation"' + ], + "✅ Good", + [ + '"Add search: filter by email, case-insensitive, return paginated"', + '"Fix: login returns 500 when email contains + character"', + '"Add pagination: 20 per page, cursor-based, sort by created_at"', + '"Validate: email format, password 8+ chars, both required"' + ] + ) + page_num += 1 + + # ================= + # PART 6: FINAL CHALLENGE + # ================= + create_section_slide(story, styles, "Part 6: Final Challenge") + page_num += 1 + + create_content_slide(story, styles, "Final Exercise: Complete Feature", [ + ("Build ONE of these features end-to-end:", [ + "Option A: GET /api/auth/me - Get current user profile", + "Option B: PUT /api/auth/password - Change password", + "Option C: POST /api/auth/logout - Logout (invalidate token)" + ]), + ("Follow the full workflow:", [ + "1. Write specification", + "2. Update database if needed", + "3. Implement repository and handler", + "4. Write E2E tests", + "5. Add unit tests if applicable", + "6. Commit and add to PR" + ]), + "Time: 25 minutes" + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Final Challenge: Your Feature", +"""Choose ONE feature and implement it fully: + +OPTION A: GET /api/auth/me +- Returns current user info from JWT token +- Response: { id, email, created_at } +- Tests: valid token returns user, invalid token returns 401 + +OPTION B: PUT /api/auth/password +- Request: { current_password, new_password } +- Verify current password, update to new +- Tests: success, wrong current password, weak new password + +OPTION C: POST /api/auth/logout +- Invalidate current token (add to blacklist) +- Tests: logout succeeds, token no longer works + +Steps: +1. Tell AI which feature you chose +2. Have AI implement it +3. Review the code +4. Run tests +5. Commit with conventional format + +Time limit: 25 minutes""", + "New feature implemented with passing tests and committed", + page_num, ALL_PROMPTS) + page_num += 1 + + # ================= + # WRAP-UP + # ================= + create_section_slide(story, styles, "Wrap-Up") + page_num += 1 + + create_content_slide(story, styles, "What We Built Today", [ + "✓ Project from scratch with proper structure", + "✓ Project rules file (CLAUDE.md)", + "✓ User authentication feature", + "✓ Database migration", + "✓ Repository and API layers", + "✓ E2E tests (7+ test cases)", + "✓ Unit tests", + "✓ Proper Git workflow", + "✓ Pull request ready for review" + ]) + page_num += 1 + + create_content_slide(story, styles, "Key Takeaways", [ + "AI executes 100%, Human validates 100%", + "Always write specs BEFORE AI implements", + "Review every piece of AI-generated code", + "Tests are mandatory (E2E + Unit)", + "Use project rules file to maintain consistency", + "Conventional commits and proper Git workflow", + "Break complex tasks into smaller steps" + ]) + page_num += 1 + + create_content_slide(story, styles, "Resources & Next Steps", [ + ("Reference Repository:", [ + "github.com/emmanuelandre/unveiling-claude", + "Study the patterns, adapt for your projects" + ]), + ("AI Tools:", [ + "Claude Code: claude.ai/code", + "Windsurf: codeium.com/windsurf", + "Cursor: cursor.sh" + ]), + ("Your Next Project:", [ + "Start with a project rules file", + "Use the 10-step workflow", + "Build good habits from day one" + ]) + ]) + page_num += 1 + + # ================= + # THANK YOU + # ================= + create_thank_you_slide(story, styles, "Thank You!", "Happy Building! 🚀") + + # Build PDF + doc.build(story, canvasmaker=NumberedCanvas) + print("✅ Workshop presentation created: ai-first-workshop.pdf") + + # Export prompts + export_prompts_to_markdown( + ALL_PROMPTS, + "workshop-prompts.md", + "AI-First Workshop Prompts" + ) + print("✅ Workshop prompts exported: workshop-prompts.md") + + +if __name__ == "__main__": + create_presentation() diff --git a/create_lecture_presentation.py b/create_lecture_presentation.py new file mode 100644 index 0000000..3ac9cac --- /dev/null +++ b/create_lecture_presentation.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python3 +""" +Generate 1-hour AI-First Development lecture presentation. +No hands-on exercises - practitioner-level depth with actionable takeaways. +Tool-agnostic with Claude Code/Windsurf as examples. +""" + +from reportlab.lib.styles import getSampleStyleSheet +from presentation_utils import ( + create_document, + create_title_slide, + create_section_slide, + create_content_slide, + create_two_column_slide, + create_code_slide, + create_thank_you_slide, + NumberedCanvas +) + + +def create_presentation(): + """Generate the complete lecture presentation.""" + doc = create_document("ai-first-lecture.pdf") + styles = getSampleStyleSheet() + story = [] + + # ================= + # TITLE + # ================= + create_title_slide( + story, styles, + "AI-First Development", + "A Practitioner's Guide", + "Applicable to Claude Code, Windsurf, Cursor, and other AI coding assistants" + ) + + # ================= + # AGENDA + # ================= + create_content_slide(story, styles, "Agenda", [ + "1. AI-First Philosophy", + "2. Addressing Common Concerns", + "3. Prompt Engineering vs Vibe Coding", + "4. Getting Started with Prompt Engineering", + "5. Scaling to Large Projects", + "6. Best Practices for Feature Documentation", + "7. Testing Strategies", + "8. Project Planning & Workflow", + "9. Git Best Practices", + "10. Tips & Tricks" + ]) + + # ================= + # SECTION 1: AI-FIRST PHILOSOPHY + # ================= + create_section_slide(story, styles, "1. AI-First Philosophy") + + create_content_slide(story, styles, "What is AI-First Development?", [ + "Traditional: Human writes code → AI assists occasionally", + "AI-First: AI executes 100% → Human validates 100%", + ("Key distinction:", [ + "AI handles ALL coding, testing, documentation", + "Human handles ALL validation, review, decisions", + "Clear handoff points at each step" + ]), + "Takeaway: Humans become architects & validators, AI becomes the builder" + ]) + + create_content_slide(story, styles, "Core Development Principles", [ + ("Specifications Drive Implementation:", [ + "Database schema → API contracts → UI components", + "Write specs BEFORE AI implements" + ]), + ("Micro-Teams of 2:", [ + "2 humans + AI assistant = redundancy without overhead", + "Each team owns end-to-end features" + ]), + ("Own Your Stack:", [ + "Be your own QA engineer", + "Be your own DevOps engineer", + "Own the entire vertical slice" + ]) + ]) + + create_content_slide(story, styles, "The Paradigm Shift", [ + ("From:", [ + '"AI helps me write code"', + '"Let me ask AI to fix this bug"', + '"AI suggests completions"' + ]), + ("To:", [ + '"AI executes my specifications"', + '"I validate what AI produced"', + '"AI implements the full feature"' + ]), + "Takeaway: Treat AI as your execution engine, not just an assistant" + ]) + + create_content_slide(story, styles, "When AI-First Works Best", [ + ("Ideal scenarios:", [ + "New features with clear requirements", + "CRUD operations and API endpoints", + "Test generation and documentation", + "Refactoring with defined patterns" + ]), + ("Less ideal scenarios:", [ + "Real-time pair programming", + "Highly visual design work", + "Hardware-specific debugging" + ]) + ]) + + # ================= + # SECTION 2: ADDRESSING COMMON CONCERNS + # ================= + create_section_slide(story, styles, "2. Addressing Common Concerns") + + create_content_slide(story, styles, "Common Concerns Engineers Raise", [ + "1. Code Quality & Reliability - Hallucinations, hidden bugs", + "2. Security Risks - Insecure patterns, data exposure", + "3. Maintainability - Opaque code, inconsistent style", + "4. Design Integrity - Architecture drift, loss of intent", + "5. Testing & Validation - False sense of coverage", + "6. IP & Compliance - License ambiguity", + "7. Developer Experience - Skill atrophy", + "8. Accountability - Who owns AI-generated bugs?" + ]) + + create_content_slide(story, styles, "Code Quality & Security", [ + ("The Concern:", [ + "AI generates syntactically correct but logically flawed code", + "AI might introduce vulnerabilities" + ]), + ("How We Address It:", [ + "Human validates 100% - every line is reviewed", + "Mandatory E2E tests catch integration issues", + "Pre-commit checks enforce quality gates", + "Three-layer review: Self → Automated → Peer" + ]), + "Takeaway: Trust but verify - always review AI output" + ]) + + create_content_slide(story, styles, "Maintainability & Design", [ + ("The Concern:", [ + "AI-generated code hard to understand", + "May not follow team conventions", + "Optimizes locally, not holistically" + ]), + ("How We Address It:", [ + "Project rules file defines conventions (CLAUDE.md, .windsurfrules)", + "Architecture documented upfront", + "Human writes specs BEFORE AI implements", + "AI follows existing patterns" + ]), + "Takeaway: Document your standards where AI can read them" + ]) + + create_content_slide(story, styles, "Testing, IP & Accountability", [ + ("Testing:", [ + "Test-first approach - define tests before implementation", + "Human verifies test quality, not just coverage" + ]), + ("IP & Compliance:", [ + "Review AI suggestions for license issues", + "Keep audit trail in commit history" + ]), + ("Accountability:", [ + "Human approves every PR = human owns the code", + "Git history shows human review at each step" + ]), + "Takeaway: Human approval = Human ownership" + ]) + + create_content_slide(story, styles, "The Bottom Line", [ + "AI-First ≠ AI-Only", + ("The methodology addresses concerns through:", [ + "Systematic validation at every step", + "Comprehensive testing (E2E + Unit)", + "Clear human ownership and accountability", + "Documented conventions in project rules" + ]), + "Result: Faster development WITH maintained quality" + ]) + + # ================= + # SECTION 3: PROMPT VS VIBE + # ================= + create_section_slide(story, styles, "3. Prompt Engineering vs Vibe Coding") + + create_content_slide(story, styles, "What is Vibe Coding?", [ + "Informal, exploratory approach with minimal instructions", + ("Characteristics:", [ + "Speed over precision", + 'AI "guesses" intent', + "Minimal context provided" + ]), + 'Example: "Make something that sorts numbers"', + ("Result:", [ + "Quick for prototypes", + "Unpredictable code quality" + ]) + ]) + + create_content_slide(story, styles, "What is Prompt Engineering?", [ + "Precise, structured inputs to guide AI behavior", + ("Characteristics:", [ + "Context and constraints provided", + "Clear success criteria", + "Examples included when helpful" + ]), + 'Example: "Write Python function to sort integers ascending, no built-in sort, return new list"', + "Result: Predictable, production-ready code" + ]) + + create_two_column_slide( + story, styles, + "Side-by-Side Comparison", + "Vibe Coding", + [ + "Fast initial results", + "Unpredictable quality", + "Best for prototypes", + "More rework later", + "Hard to maintain" + ], + "Prompt Engineering", + [ + "Slower initial setup", + "Consistent quality", + "Best for production", + "Less rework overall", + "Easy to maintain" + ] + ) + + create_content_slide(story, styles, "When to Use Each", [ + ("Use Vibe Coding for:", [ + "Quick prototypes and experiments", + "Exploring APIs or libraries", + "Throwaway code" + ]), + ("Use Prompt Engineering for:", [ + "Production code", + "Team projects", + "Anything that will be maintained", + "Code that needs tests" + ]), + "Takeaway: Default to prompt engineering for professional work" + ]) + + # ================= + # SECTION 4: PROMPT ENGINEERING + # ================= + create_section_slide(story, styles, "4. Getting Started with Prompt Engineering") + + create_content_slide(story, styles, "Core Prompt Principles", [ + ("Be Specific:", [ + 'Not "fix the bug" but "fix the race condition in token refresh"' + ]), + ("Provide Context:", [ + "Architecture, patterns, constraints", + "Reference specific files" + ]), + ("Define Success:", [ + "What should work when done?", + "What tests should pass?" + ]), + "Takeaway: Treat prompts like specifications" + ]) + + create_two_column_slide( + story, styles, + "Good vs Bad Prompts", + "❌ Bad Prompts", + [ + '"Add search to the API"', + '"Fix the authentication"', + '"Make it faster"', + '"Add validation"' + ], + "✅ Good Prompts", + [ + '"Add search: filter by email/name, case-insensitive, debounce 300ms"', + '"Fix token refresh race condition when concurrent API calls"', + '"Add pagination to /users endpoint, 20 per page, cursor-based"', + '"Validate email format and password min 8 chars on registration"' + ] + ) + + create_content_slide(story, styles, "Prompt Patterns", [ + ("Feature Request:", [ + "Requirements → Technical Details → Success Criteria" + ]), + ("Bug Fix:", [ + "Expected vs Actual → Steps to Reproduce → Error Message" + ]), + ("Multi-Step:", [ + "Break into numbered steps", + "Define validation at each step" + ]), + "Takeaway: Use templates for consistent results" + ]) + + create_code_slide(story, styles, "Example: Well-Structured Prompt", "prompt", +"""I need to implement user authentication for my API. + +Requirements: +- Users register with email/password +- JWT tokens for authentication +- Password hashing with bcrypt + +API Endpoints: +POST /api/auth/register - Register new user +POST /api/auth/login - Login and get JWT + +Success Criteria: +- Passwords never stored in plain text +- JWT expires after 1 hour +- E2E tests cover happy path and error cases + +Please create the database migration first.""") + + create_content_slide(story, styles, "Common Prompt Mistakes", [ + ("Over-reliance:", [ + '"Build me a complete e-commerce platform"', + "Fix: Break into smaller, specific requests" + ]), + ("Under-specification:", [ + '"Add validation"', + "Fix: Specify what to validate and how" + ]), + ("Forgetting Tests:", [ + '"Implement user authentication"', + 'Fix: Add "Include E2E tests for..."' + ]) + ]) + + # ================= + # SECTION 5: SCALING LARGE PROJECTS + # ================= + create_section_slide(story, styles, "5. Scaling to Large Projects") + + create_content_slide(story, styles, "The Challenge of Large Projects", [ + ("When projects grow:", [ + "100+ tasks across modules", + "Complex dependencies", + "Context loss between sessions" + ]), + ("The Solution:", [ + "Phased development", + "Centralized planning structure", + "Continuous progress tracking" + ]) + ]) + + create_content_slide(story, styles, "Project Planning Structure", [ + ("Directory Layout:", [ + "project/planning/ - Master plan and progress", + "project/specs/ - Feature specifications", + "project/sessions/ - Session summaries" + ]), + ("Key Files:", [ + "devplan.md - Master plan with phases and dependencies", + "devprogress.md - Progress tracker with checkboxes", + "database.md - Schema documentation" + ]), + "Takeaway: Organize documentation for AI consumption" + ]) + + create_content_slide(story, styles, "The Master Plan (devplan.md)", [ + ("Contains:", [ + "Phases with clear objectives", + "Dependency mapping between features", + "Tasks with checkboxes" + ]), + ("Workflow per Feature:", [ + "DB → API → Tests → UI → UI Tests", + "Complete each layer before moving on", + "Never skip testing" + ]) + ]) + + create_content_slide(story, styles, "Progress Tracking (devprogress.md)", [ + ("Update After Every Session:", [ + "Mark completed tasks [x]", + "Update phase percentages", + "Document blockers" + ]), + ("Quick Stats Table:", [ + "Phase | Status | Progress", + "🔴 Not Started | 🟡 In Progress | 🟢 Complete" + ]), + "Takeaway: Update progress religiously - it's your context for next session" + ]) + + create_content_slide(story, styles, "When to Use What", [ + ("Small Projects (<20 tasks):", [ + "Simple project rules file is enough" + ]), + ("Medium Projects (20-50 tasks):", [ + "Add devplan.md and devprogress.md" + ]), + ("Large Projects (50+ tasks):", [ + "Full planning structure", + "Session notes after every session" + ]), + "Takeaway: Scale documentation with project complexity" + ]) + + # ================= + # SECTION 6: FEATURE DOCUMENTATION + # ================= + create_section_slide(story, styles, "6. Best Practices for Feature Documentation") + + create_content_slide(story, styles, "The 5-Document Approach", [ + "For large features, create 5 documents:", + ("1. Planning Document (What & When):", [ + "Timeline, scope, decisions, success criteria" + ]), + ("2. Architecture Document (How):", [ + "System diagrams, database schema, error handling" + ]), + ("3. API Contracts (Interface):", [ + "Endpoints, request/response examples, error codes" + ]), + ("4. User Journey (Experience):", [ + "Personas, step-by-step flows, edge cases" + ]), + ("5. UI Wireframes (Visuals):", [ + "Page layouts, component specs, responsive design" + ]) + ]) + + create_content_slide(story, styles, "Planning Document Structure", [ + ("Key Sections:", [ + "Related Documentation - links to other docs", + "Overview - problem statement and solution", + "Key Decisions - with rationale", + "Scope - what's in and explicitly out", + "Implementation Phases - with tasks", + "Critical Files - all files to create/modify", + "Success Criteria - measurable goals" + ]), + "Takeaway: Explicit scope prevents scope creep" + ]) + + create_content_slide(story, styles, "Architecture & API Contracts", [ + ("Architecture Document:", [ + "ASCII system diagrams showing component interactions", + "Complete database schema with indexes", + "Error handling strategies" + ]), + ("API Contracts:", [ + "Endpoint summary table", + "Request/response JSON examples", + "Error codes with descriptions" + ]), + "Takeaway: AI produces better code with precise contracts" + ]) + + create_content_slide(story, styles, "Design Review Process", [ + ("Review Stages:", [ + "Draft → Review → Approved → Implement" + ]), + ("Technical Review Focus:", [ + "Architecture - does it integrate cleanly?", + "API Design - RESTful and consistent?", + "Database - indexes sufficient?", + "Security - auth and validation complete?" + ]), + "Takeaway: Catch issues early when changes are cheap" + ]) + + # ================= + # SECTION 7: TESTING STRATEGIES + # ================= + create_section_slide(story, styles, "7. Testing Strategies") + + create_content_slide(story, styles, "Modern Testing Philosophy", [ + "Both E2E and Unit tests are MANDATORY", + ("E2E Tests (Required):", [ + "Complete user journeys", + "API endpoint testing", + "UI workflow testing" + ]), + ("Unit Tests (Required):", [ + "Business logic", + "Utilities and helpers", + "Edge cases and data transformations" + ]), + ("Component Tests (When applicable):", [ + "Microservices integration", + "Database operations with test containers" + ]) + ]) + + create_content_slide(story, styles, "Test-First Approach", [ + ("Before Implementation:", [ + "Define coverage targets (70-90%)", + "Build testing infrastructure first", + "Document test scenarios" + ]), + ("During Implementation:", [ + "Write tests alongside code", + "Run tests frequently", + "Don't proceed if tests fail" + ]), + "Takeaway: Tests are your regression safety net" + ]) + + create_content_slide(story, styles, "Testing Best Practices", [ + "Test user journeys, not implementation details", + "Use data-test attributes for stable selectors", + ("Coverage priorities:", [ + "Happy path - must pass", + "Critical failures - invalid inputs, permissions", + "Edge cases - boundary conditions", + "Error scenarios - network failures, timeouts" + ]), + "Run tests in pre-commit hooks", + "Takeaway: Tests that run automatically get run consistently" + ]) + + create_content_slide(story, styles, "Coverage Measurement", [ + ("Track coverage from all test types:", [ + "Unit test coverage", + "E2E test coverage", + "Component test coverage (if applicable)" + ]), + ("Merge coverage reports:", [ + "See the complete picture", + "Identify gaps" + ]), + "Takeaway: Measure coverage from ALL test types combined" + ]) + + # ================= + # SECTION 8: PROJECT PLANNING + # ================= + create_section_slide(story, styles, "8. Project Planning & Workflow") + + create_content_slide(story, styles, "The 10-Step Development Process", [ + "1. Specification (Human writes detailed spec)", + "2. Database Schema (AI designs → Human reviews)", + "3. Repository Layer (AI implements → Human reviews)", + "4. API Endpoints (AI creates → Human reviews)", + "5. API E2E Tests (AI writes → Human verifies)", + "6. Frontend Components (AI builds → Human reviews)", + "7. UI E2E Tests (AI creates → Human verifies)", + "8. Documentation (AI updates → Human reviews)", + "9. Code Review (Human conducts)", + "10. Deployment (AI executes → Human verifies)" + ]) + + create_content_slide(story, styles, "Handoffs & Quality Gates", [ + ("Each Step Has:", [ + "Clear deliverables", + "Human approval checkpoint", + "Tests that must pass" + ]), + ("Quality Gates:", [ + "Schema reviewed before repository", + "API tests pass before frontend", + "All tests pass before PR" + ]), + "Takeaway: Never skip a step, never skip a review" + ]) + + create_two_column_slide( + story, styles, + "AI vs Human Responsibilities", + "AI Executes", + [ + "Design database schema", + "Implement repository layer", + "Create API endpoints", + "Write tests", + "Build UI components", + "Update documentation", + "Execute deployment" + ], + "Human Validates", + [ + "Write specifications", + "Review all code", + "Verify tests are meaningful", + "Conduct code review", + "Merge pull requests", + "Make architectural decisions" + ] + ) + + # ================= + # SECTION 9: GIT BEST PRACTICES + # ================= + create_section_slide(story, styles, "9. Git Best Practices") + + create_content_slide(story, styles, "Branch Naming & Commits", [ + ("Branch Naming: /", [ + "feature/user-auth", + "fix/login-bug", + "refactor/api-cleanup", + "docs/readme-update" + ]), + ("Conventional Commits: (): ", [ + "feat(auth): add Google OAuth login", + "fix(api): handle null user gracefully", + "test(auth): add E2E tests for login flow" + ]), + "Takeaway: Consistent naming enables automation" + ]) + + create_content_slide(story, styles, "Pre-Commit Checks (Mandatory)", [ + ("Must Run Before Every Commit:", [ + "Lint - code formatting and style", + "Tests - unit and E2E tests", + "Build - verify it compiles/bundles" + ]), + ("Set Up Hooks:", [ + "Use husky (Node) or pre-commit (Python)", + "Fail commit if any check fails" + ]), + "Takeaway: Catch issues locally, not in PR review" + ]) + + create_content_slide(story, styles, "Hard Rules", [ + "❌ Never commit directly to main", + "❌ Never merge your own PR without review", + "❌ Never commit code that fails tests", + "❌ Never commit secrets (.env, credentials)", + "❌ Never force push to main", + ("AI Boundaries:", [ + "✅ AI can: create branches, commit, push, open PRs", + "❌ AI cannot: merge PRs, approve PRs" + ]), + "Takeaway: Humans control what gets merged" + ]) + + # ================= + # SECTION 10: TIPS & TRICKS + # ================= + create_section_slide(story, styles, "10. Tips & Tricks") + + create_content_slide(story, styles, "Communication Tips", [ + ("Be Specific:", [ + '"Fix the race condition in token refresh" not "fix the bug"' + ]), + ("Reference Files:", [ + '"In src/models/user.ts, add email_verified field"' + ]), + ("Request Explanations:", [ + '"Implement and explain the trade-offs"' + ]), + ("Verify Understanding:", [ + '"Before implementing, confirm: Goal is X, constraints are Y"' + ]) + ]) + + create_content_slide(story, styles, "Productivity Techniques", [ + ("Multi-Step Requests:", [ + "Number your steps", + "Define validation at each step" + ]), + ("Batch Related Changes:", [ + '"Update all error responses to use format X"' + ]), + ("Progressive Refinement:", [ + "Basic → Add feature → Add polish" + ]), + ("Start Sessions with Context:", [ + '"Continuing work on X. Last session we did Y."' + ]) + ]) + + create_content_slide(story, styles, "Working with Large Codebases", [ + ("Navigate:", [ + '"Show me all files that handle authentication"' + ]), + ("Understand First:", [ + '"Explain how the current auth flow works"' + ]), + ("Refactor Safely:", [ + '"Refactor X to Y. Ensure all existing tests pass."' + ]), + "Takeaway: Use AI to explore unfamiliar code" + ]) + + create_content_slide(story, styles, "Pro Tips Summary", [ + "Start sessions with context", + "End sessions with notes (what was done, what's next)", + "Use checkpoints - commit after each logical step", + "Trust but verify - always review AI output", + "Keep project rules file current", + "Build a prompt library for your team", + "Takeaway: Document what prompts work for your project" + ]) + + # ================= + # SUMMARY + # ================= + create_section_slide(story, styles, "Summary & Action Plan") + + create_content_slide(story, styles, "Key Takeaways", [ + "✓ AI-First: AI executes 100%, Human validates 100%", + "✓ Prompt Engineering > Vibe Coding for production", + "✓ Project rules file is essential", + "✓ 10-step workflow with clear handoffs", + "✓ E2E + Unit tests are both mandatory", + "✓ Pre-commit checks are non-negotiable", + "✓ Humans control what gets merged" + ]) + + create_content_slide(story, styles, "Your Action Plan", [ + ("Start Today:", [ + "Create a project rules file for your current project" + ]), + ("Next Feature:", [ + "Use the 10-step process", + "Write specs BEFORE AI implements" + ]), + ("As Projects Grow:", [ + "Add planning structure (devplan.md, devprogress.md)" + ]), + ("Resources:", [ + "Claude Code: claude.ai/code", + "Windsurf: codeium.com/windsurf", + "Cursor: cursor.sh" + ]) + ]) + + # ================= + # THANK YOU + # ================= + create_thank_you_slide(story, styles, "Thank You!", "Questions?") + + # Build PDF + doc.build(story, canvasmaker=NumberedCanvas) + print("✅ Lecture presentation created: ai-first-lecture.pdf") + + +if __name__ == "__main__": + create_presentation() diff --git a/presentation_utils.py b/presentation_utils.py new file mode 100644 index 0000000..692ef2b --- /dev/null +++ b/presentation_utils.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +""" +Shared utilities for generating AI-First Development presentations. +Extracted from create_interactive_presentation_v2.py for reuse across presentations. +""" + +from reportlab.lib.pagesizes import landscape +from reportlab.lib.units import inch +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.enums import TA_LEFT, TA_CENTER +from reportlab.pdfgen import canvas +from reportlab.lib.colors import HexColor + +# 16:9 aspect ratio +PAGESIZE = (11.0 * inch, 6.1875 * inch) + +# Color scheme +PRIMARY_BLUE = HexColor('#0076CE') +DARK_BLUE = HexColor('#003E7E') +LIGHT_BLUE = HexColor('#48B9E8') +DARK_GRAY = HexColor('#5B5B5B') +MED_GRAY = HexColor('#999999') +LIGHT_GRAY = HexColor('#E5E5E5') +WHITE = HexColor('#FFFFFF') +SUCCESS_GREEN = HexColor('#00B388') +WARNING_ORANGE = HexColor('#FF8300') + + +class NumberedCanvas(canvas.Canvas): + """Custom canvas that adds page numbers to each page.""" + + def __init__(self, *args, **kwargs): + canvas.Canvas.__init__(self, *args, **kwargs) + self._saved_page_states = [] + + def showPage(self): + self._saved_page_states.append(dict(self.__dict__)) + self._startPage() + + def save(self): + num_pages = len(self._saved_page_states) + for state in self._saved_page_states: + self.__dict__.update(state) + self.draw_page_number(num_pages) + canvas.Canvas.showPage(self) + canvas.Canvas.save(self) + + def draw_page_number(self, page_count): + self.setStrokeColor(PRIMARY_BLUE) + self.setLineWidth(2) + self.line(0.5*inch, 0.4*inch, PAGESIZE[0] - 0.5*inch, 0.4*inch) + + self.setFont("Helvetica", 9) + self.setFillColor(MED_GRAY) + page = "Page %d of %d" % (self._pageNumber, page_count) + self.drawRightString(PAGESIZE[0] - 0.5*inch, 0.25*inch, page) + + +def create_document(output_filename): + """Create a SimpleDocTemplate with standard settings.""" + return SimpleDocTemplate( + output_filename, + pagesize=PAGESIZE, + rightMargin=0.5*inch, + leftMargin=0.5*inch, + topMargin=0.7*inch, + bottomMargin=0.7*inch + ) + + +def create_title_slide(story, styles, title, subtitle, description=None): + """Create a title slide with centered content.""" + story.append(Spacer(1, 1.5*inch)) + + title_para = Paragraph(f'{title}', styles['Title']) + story.append(title_para) + story.append(Spacer(1, 0.2*inch)) + + subtitle_para = Paragraph(f'{subtitle}', styles['Title']) + story.append(subtitle_para) + + if description: + story.append(Spacer(1, 0.2*inch)) + desc_para = Paragraph(f'{description}', styles['Title']) + story.append(desc_para) + + story.append(PageBreak()) + + +def create_section_slide(story, styles, title): + """Create a section divider slide with blue background.""" + story.append(Spacer(1, 1.8*inch)) + + section_text = Paragraph( + f'{title}', + ParagraphStyle('SectionContent', parent=styles['Normal'], fontSize=38, + textColor=WHITE, alignment=TA_CENTER, leading=48, + spaceAfter=30, spaceBefore=30) + ) + + data = [[section_text]] + table = Table(data, colWidths=[9.5*inch]) + table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, -1), PRIMARY_BLUE), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('TOPPADDING', (0, 0), (-1, -1), 40), + ('BOTTOMPADDING', (0, 0), (-1, -1), 40), + ])) + + story.append(table) + story.append(PageBreak()) + + +def create_content_slide(story, styles, title, content_items): + """ + Create a standard content slide with bullets. + + content_items can be: + - A string: Creates a bullet point + - A tuple (header, list): Creates a header with sub-bullets + """ + slide_title = Paragraph(f'{title}', styles['Heading1']) + story.append(slide_title) + story.append(Spacer(1, 0.3*inch)) + + for item in content_items: + if isinstance(item, str): + p = Paragraph(f'• {item}', styles['BodyText']) + story.append(p) + story.append(Spacer(1, 0.15*inch)) + elif isinstance(item, tuple): + p = Paragraph(f'{item[0]}', styles['BodyText']) + story.append(p) + story.append(Spacer(1, 0.1*inch)) + for sub in item[1]: + sp = Paragraph(f' ◦ {sub}', styles['BodyText']) + story.append(sp) + story.append(Spacer(1, 0.08*inch)) + story.append(Spacer(1, 0.1*inch)) + + story.append(PageBreak()) + + +def create_two_column_slide(story, styles, title, left_title, left_items, right_title, right_items): + """Create a two-column comparison slide.""" + slide_title = Paragraph(f'{title}', styles['Heading1']) + story.append(slide_title) + story.append(Spacer(1, 0.3*inch)) + + # Build left column content + left_content = [Paragraph(f'{left_title}', styles['BodyText'])] + for item in left_items: + left_content.append(Spacer(1, 0.1*inch)) + left_content.append(Paragraph(f'• {item}', styles['BodyText'])) + + # Build right column content + right_content = [Paragraph(f'{right_title}', styles['BodyText'])] + for item in right_items: + right_content.append(Spacer(1, 0.1*inch)) + right_content.append(Paragraph(f'• {item}', styles['BodyText'])) + + # Create table + data = [[left_content, right_content]] + table = Table(data, colWidths=[4.5*inch, 4.5*inch]) + table.setStyle(TableStyle([ + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LEFTPADDING', (0, 0), (-1, -1), 10), + ('RIGHTPADDING', (0, 0), (-1, -1), 10), + ])) + + story.append(table) + story.append(PageBreak()) + + +def create_hands_on_slide(story, styles, title, prompt, expected_result, page_num, prompts_list=None): + """ + Create a hands-on exercise slide with prompt and expected result. + + If prompts_list is provided, appends the prompt to it for export. + """ + # Store prompt for export if list provided + if prompts_list is not None: + prompts_list.append({ + 'page': page_num, + 'title': title, + 'prompt': prompt, + 'expected': expected_result + }) + + slide_title = Paragraph(f'🔨 Hands-On: {title}', styles['Heading1']) + story.append(slide_title) + story.append(Spacer(1, 0.2*inch)) + + # Instruction + instruction = Paragraph('YOUR PROMPT:', styles['BodyText']) + story.append(instruction) + story.append(Spacer(1, 0.15*inch)) + + # Format prompt with line breaks preserved + prompt_html = prompt.replace('\n', '
') + + # Prompt box with preformatted style + prompt_para = Paragraph( + f'{prompt_html}', + ParagraphStyle('PromptBox', + parent=styles['BodyText'], + leftIndent=20, + rightIndent=20, + backColor=LIGHT_GRAY, + spaceBefore=5, + spaceAfter=5, + leading=14) + ) + story.append(prompt_para) + story.append(Spacer(1, 0.2*inch)) + + # Expected result + result = Paragraph('What You Should See:', styles['BodyText']) + story.append(result) + story.append(Spacer(1, 0.1*inch)) + + result_text = Paragraph(f'• {expected_result}', styles['BodyText']) + story.append(result_text) + + story.append(PageBreak()) + + +def create_code_slide(story, styles, title, language, code): + """Create a code example slide.""" + slide_title = Paragraph(f'{title}', styles['Heading1']) + story.append(slide_title) + story.append(Spacer(1, 0.25*inch)) + + # Language label + lang_label = Paragraph(f'{language.upper()}', styles['BodyText']) + story.append(lang_label) + story.append(Spacer(1, 0.1*inch)) + + # Code with line breaks preserved + code_html = code.replace('\n', '
').replace(' ', ' ') + code_para = Paragraph( + f'{code_html}', + ParagraphStyle('CodeBox', + parent=styles['BodyText'], + leftIndent=15, + rightIndent=15, + backColor=LIGHT_GRAY, + spaceBefore=5, + spaceAfter=5, + leading=12) + ) + story.append(code_para) + story.append(PageBreak()) + + +def create_thank_you_slide(story, styles, title="Thank You!", subtitle="Questions?"): + """Create a thank you/closing slide.""" + story.append(Spacer(1, 2*inch)) + thank_you = Paragraph(f'{title}', styles['Title']) + story.append(thank_you) + story.append(Spacer(1, 0.4*inch)) + + questions = Paragraph(f'{subtitle}', styles['Title']) + story.append(questions) + + +def export_prompts_to_markdown(prompts_list, output_filename, title="Workshop Prompts"): + """Export collected prompts to a markdown file.""" + with open(output_filename, 'w') as f: + f.write(f'# {title}\n\n') + f.write('Copy and paste these prompts during the workshop exercises.\n\n') + f.write('---\n\n') + + for item in prompts_list: + f.write(f'## Page {item["page"]}: {item["title"]}\n\n') + f.write('**PROMPT:**\n```\n') + f.write(item['prompt']) + f.write('\n```\n\n') + f.write(f'**EXPECTED RESULT:**\n') + f.write(f'{item["expected"]}\n\n') + f.write('---\n\n') diff --git a/workshop-prompts.md b/workshop-prompts.md new file mode 100644 index 0000000..7bbf4f4 --- /dev/null +++ b/workshop-prompts.md @@ -0,0 +1,451 @@ +# AI-First Workshop Prompts + +Copy and paste these prompts during the workshop exercises. + +--- + +## Page 9: Exercise 1: Create Your Repository + +**PROMPT:** +``` +Create a new GitHub repository for your project: + +1. Go to github.com and create a new PRIVATE repository + Name: my-ai-project (or your preferred name) + +2. Clone it locally: + git clone https://github.com/[YOUR-USERNAME]/my-ai-project.git + cd my-ai-project + +3. Verify: + git status + Should show: "On branch main, nothing to commit" + +Reference: Check unveiling-claude repo structure for ideas +``` + +**EXPECTED RESULT:** +Local repository initialized and connected to GitHub + +--- + +## Page 10: Exercise 2: Create Project Rules File + +**PROMPT:** +``` +Ask your AI assistant: + +Help me create a project rules file for my project. + +Project details: +- Language: [Go/Node/Python] +- Type: REST API with database +- Database: PostgreSQL (or SQLite for simplicity) +- Testing: E2E with [Cypress/Go test/pytest] + +Include these sections: +1. Project Overview (what this project does) +2. Tech Stack (language, framework, database) +3. Project Structure (recommended directories) +4. Commands (build, test, run) +5. Git Workflow (branch naming, commit format) +6. Testing Requirements (E2E mandatory) + +Save as CLAUDE.md (or .windsurfrules for Windsurf) +``` + +**EXPECTED RESULT:** +Project rules file created with all sections + +--- + +## Page 12: Exercise 3: Initialize Git Workflow + +**PROMPT:** +``` +Ask your AI assistant: + +Set up the git workflow for my project: + +1. Create .gitignore for [Go/Node/Python] project + Include: build outputs, dependencies, IDE files, .env + +2. Create initial directory structure: + - cmd/ or src/ for source code + - tests/ or test/ for tests + - migrations/ for database migrations + - docs/ for documentation + +3. Create initial commit: + git add . + git commit -m "chore: initial project setup" + +4. Create feature branch for our first feature: + git checkout -b feature/user-auth +``` + +**EXPECTED RESULT:** +Git initialized with proper structure and on feature branch + +--- + +## Page 16: Step 1: Write Feature Specification + +**PROMPT:** +``` +Ask your AI assistant: + +I need to implement user authentication for my API. + +Requirements: +- Users register with email and password +- Users login with email and password +- JWT tokens for authentication +- Password hashing (never store plain text) + +Database Schema Needed: +- users table: id, email, password_hash, created_at, updated_at + +API Endpoints: +POST /api/auth/register - Register new user + Request: { email, password } + Response: { user_id, email, token } + +POST /api/auth/login - Login existing user + Request: { email, password } + Response: { user_id, email, token } + +Success Criteria: +- Passwords hashed with bcrypt +- JWT expires after 1 hour +- Proper HTTP status codes (201, 200, 400, 401) +- E2E tests cover happy path and errors + +Please confirm you understand before we proceed. +``` + +**EXPECTED RESULT:** +AI confirms understanding, may ask clarifying questions + +--- + +## Page 18: Step 2: Create Database Schema + +**PROMPT:** +``` +Ask your AI assistant: + +Create the database migration for the users table. + +Requirements: +- Table name: users +- Columns: id (primary key), email (unique), password_hash, created_at, updated_at +- Add index on email column for fast lookups + +For Go: Create migrations/001_create_users.up.sql and .down.sql +For Node: Create migrations/001_create_users.js +For Python: Create migrations/001_create_users.py + +Use appropriate types for your database: +- PostgreSQL: SERIAL, VARCHAR, TIMESTAMP +- SQLite: INTEGER PRIMARY KEY, TEXT + +Include both up (create) and down (drop) migrations. +``` + +**EXPECTED RESULT:** +Migration file(s) created with proper schema + +--- + +## Page 21: Step 3: Implement Repository Layer + +**PROMPT:** +``` +Ask your AI assistant: + +Implement the repository layer for user operations. + +Create a UserRepository with these methods: +- Create(email, passwordHash) -> User +- FindByEmail(email) -> User or null +- FindByID(id) -> User or null + +Requirements: +- Use prepared statements (prevent SQL injection) +- Handle database errors gracefully +- Return appropriate errors (not found, duplicate, etc.) + +Place in: +- Go: internal/repository/user_repository.go +- Node: src/repositories/userRepository.js +- Python: app/repositories/user_repository.py + +Follow patterns from the project rules file. +``` + +**EXPECTED RESULT:** +Repository file created with all methods + +--- + +## Page 22: Step 4: Implement API Handlers + +**PROMPT:** +``` +Ask your AI assistant: + +Implement the API handlers for authentication. + +Create handlers for: +1. POST /api/auth/register + - Validate email format + - Validate password (minimum 8 characters) + - Hash password with bcrypt + - Create user in database + - Generate and return JWT token + - Return 400 for validation errors + - Return 409 for duplicate email + +2. POST /api/auth/login + - Find user by email + - Verify password against hash + - Generate and return JWT token + - Return 401 for invalid credentials + +Include: +- Input validation +- Proper HTTP status codes +- JSON response format: { success: true/false, data/error } +- Wire up routes to main application +``` + +**EXPECTED RESULT:** +Handler files created and routes configured + +--- + +## Page 25: Step 5: Write E2E API Tests + +**PROMPT:** +``` +Ask your AI assistant: + +Create E2E tests for the authentication endpoints. + +Test cases needed: +1. Register - Happy path (201) + - New user can register + - Response includes token + +2. Register - Duplicate email (409) + - Cannot register same email twice + +3. Register - Invalid email (400) + - Rejects malformed email + +4. Register - Weak password (400) + - Rejects password < 8 chars + +5. Login - Valid credentials (200) + - Returns token + +6. Login - Invalid password (401) + - Wrong password rejected + +7. Login - Non-existent user (401) + - Unknown email rejected + +Use your testing framework: +- Go: internal/handlers/auth_test.go +- Node: tests/api/auth.test.js (Jest/Vitest) +- Python: tests/test_auth.py (pytest) + +Include setup and teardown for test database. +``` + +**EXPECTED RESULT:** +Test file created with all test cases + +--- + +## Page 27: Run and Verify Tests + +**PROMPT:** +``` +Run your E2E tests: + +For Go: + go test -v ./internal/handlers/... + +For Node: + npm test -- --grep "Auth API" + +For Python: + pytest tests/test_auth.py -v + +Expected: All 7 test cases pass (green) + +If tests fail, share the error with your AI assistant: +"Test [name] is failing with this error: +[paste error output] + +Please analyze the failure and suggest a fix." + +Continue until ALL tests pass! +``` + +**EXPECTED RESULT:** +All E2E tests passing (7/7 green) + +--- + +## Page 31: Exercise: Add Unit Tests + +**PROMPT:** +``` +Ask your AI assistant: + +Add unit tests for the authentication logic. + +Create unit tests for: +1. Password validation function + - Test: 8+ chars passes + - Test: < 8 chars fails + - Test: Empty string fails + +2. Email validation function + - Test: valid@email.com passes + - Test: invalid-email fails + - Test: Empty string fails + +3. JWT token generation + - Test: Token contains user ID + - Test: Token has correct expiration + +4. Password hashing + - Test: Same password produces different hashes (salt) + - Test: Verify function works + +Place in appropriate test file: +- Go: internal/auth/auth_test.go +- Node: tests/unit/auth.test.js +- Python: tests/unit/test_auth.py +``` + +**EXPECTED RESULT:** +Unit tests created and passing + +--- + +## Page 35: Exercise: Create Proper Commits + +**PROMPT:** +``` +Ask your AI assistant: + +Help me commit my authentication feature properly. + +My changes include: +- Database migration for users table +- Repository layer +- API handlers +- E2E tests +- Unit tests + +Steps: +1. Review what's changed: git status +2. Stage all changes: git add . +3. Create commit with conventional format + +Suggested commit message: +feat(auth): add user registration and login + +- Add users table migration +- Implement user repository with CRUD +- Create register and login endpoints +- Add E2E tests for all auth endpoints +- Add unit tests for validation logic + +After committing, verify with: git log --oneline -1 +``` + +**EXPECTED RESULT:** +Commit created with proper conventional commit message + +--- + +## Page 36: Exercise: Create Pull Request + +**PROMPT:** +``` +Ask your AI assistant: + +Help me push my branch and create a pull request. + +Steps: +1. Push branch to remote: + git push -u origin feature/user-auth + +2. Create PR (with GitHub CLI): + gh pr create --title "feat: Add user authentication" \ + --body "## Summary + - User registration endpoint + - User login endpoint + - JWT token authentication + - E2E tests (7 passing) + - Unit tests for validation + + ## Testing + - All E2E tests pass + - All unit tests pass + - Manual testing completed + + ## Checklist + - [x] Code follows project conventions + - [x] Tests added + - [x] Documentation updated" + +Or create PR through GitHub web interface. +``` + +**EXPECTED RESULT:** +PR created on GitHub with description + +--- + +## Page 41: Final Challenge: Your Feature + +**PROMPT:** +``` +Choose ONE feature and implement it fully: + +OPTION A: GET /api/auth/me +- Returns current user info from JWT token +- Response: { id, email, created_at } +- Tests: valid token returns user, invalid token returns 401 + +OPTION B: PUT /api/auth/password +- Request: { current_password, new_password } +- Verify current password, update to new +- Tests: success, wrong current password, weak new password + +OPTION C: POST /api/auth/logout +- Invalidate current token (add to blacklist) +- Tests: logout succeeds, token no longer works + +Steps: +1. Tell AI which feature you chose +2. Have AI implement it +3. Review the code +4. Run tests +5. Commit with conventional format + +Time limit: 25 minutes +``` + +**EXPECTED RESULT:** +New feature implemented with passing tests and committed + +--- + From b08065deb6166dbb3f20e9057d53c5ae3ed3a6da Mon Sep 17 00:00:00 2001 From: Emmanuel Andre Date: Thu, 4 Dec 2025 20:58:48 +0800 Subject: [PATCH 4/4] feat: add open-ended workshop presentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create create_openended_workshop.py for 3-hour open-ended format - Generate ai-first-openended-workshop.pdf (53 pages) - Auto-generate openended-workshop-prompts.md with 6 exercises - Attendees choose from unveiling-claude projects or bring own idea - Minimal guidance after project selection - focus on independent work - Update CLAUDE.md and WORKSHOP_GUIDE.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 22 +- WORKSHOP_GUIDE.md | 97 ++++- ai-first-openended-workshop.pdf | Bin 0 -> 48621 bytes create_openended_workshop.py | 651 ++++++++++++++++++++++++++++++++ openended-workshop-prompts.md | 182 +++++++++ 5 files changed, 940 insertions(+), 12 deletions(-) create mode 100644 ai-first-openended-workshop.pdf create mode 100644 create_openended_workshop.py create mode 100644 openended-workshop-prompts.md diff --git a/CLAUDE.md b/CLAUDE.md index 9e691bc..a4cf3f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,14 +45,17 @@ claude-tutorial/ ├── # Presentation PDFs ├── claude-code-interactive-tutorial.pdf # Original interactive workshop (2-3 hours) ├── ai-first-lecture.pdf # 1-hour lecture (no hands-on) -├── ai-first-workshop.pdf # 3-hour hands-on workshop +├── ai-first-workshop.pdf # 3-hour hands-on workshop (guided) +├── ai-first-openended-workshop.pdf # 3-hour open-ended workshop (choose your project) +├── openended-workshop-prompts.md # Prompts for open-ended workshop │ ├── # Python scripts to generate presentations ├── presentation_utils.py # Shared utilities for PDF generation ├── create_presentation.py # Generates overview PDF ├── create_interactive_presentation_v2.py # Generates interactive tutorial PDF ├── create_lecture_presentation.py # Generates 1-hour lecture PDF -└── create_handson_workshop.py # Generates 3-hour workshop PDF + prompts +├── create_handson_workshop.py # Generates 3-hour guided workshop PDF +└── create_openended_workshop.py # Generates 3-hour open-ended workshop PDF ``` ## Key Concepts (Important for Editing) @@ -146,12 +149,19 @@ The workshop materials work together as a system. There are three presentation o - Best for: Conference talks, team briefings, executive overviews - Topics: Philosophy, concerns, prompt engineering, testing, git, tips -**3. Hands-On Workshop (3 hours)** -- `ai-first-workshop.pdf` - Focused on building from scratch +**3. Guided Hands-On Workshop (3 hours)** +- `ai-first-workshop.pdf` - Guided workshop building auth feature - `workshop-prompts.md` - Copy-paste prompts (auto-generated) +- Best for: Full training sessions with structured exercises +- Attendees follow step-by-step exercises + +**4. Open-Ended Workshop (3 hours)** +- `ai-first-openended-workshop.pdf` - Choose your own project +- `openended-workshop-prompts.md` - Prompts for open-ended format - Reference repo: `github.com/emmanuelandre/unveiling-claude` -- Best for: Full training sessions, bootcamps -- Attendees build their own project using reference examples +- Best for: Experienced developers, hackathon-style sessions +- Attendees choose from sample specs or bring their own idea +- Minimal guidance after project selection - independent work with AI **Instructor Guide:** - `WORKSHOP_GUIDE.md` - Notes for running any workshop format diff --git a/WORKSHOP_GUIDE.md b/WORKSHOP_GUIDE.md index db2e166..f806792 100644 --- a/WORKSHOP_GUIDE.md +++ b/WORKSHOP_GUIDE.md @@ -5,7 +5,8 @@ | Presentation | Duration | Format | Use Case | |--------------|----------|--------|----------| | `ai-first-lecture.pdf` | 1 hour | Lecture (no hands-on) | Conference talks, team briefings | -| `ai-first-workshop.pdf` | 3 hours | Hands-on | Training sessions, bootcamps | +| `ai-first-workshop.pdf` | 3 hours | Guided hands-on | Training sessions, bootcamps | +| `ai-first-openended-workshop.pdf` | 3 hours | Open-ended hands-on | Hackathons, experienced devs | | `claude-code-interactive-tutorial.pdf` | 2-3 hours | Interactive | Standard workshops | | `claude-code-tutorial.pdf` | 30 min | Overview | Quick introductions | @@ -13,8 +14,10 @@ ### Presentation Files - **`ai-first-lecture.pdf`** - 1-hour lecture covering all AI-first topics (NO hands-on) -- **`ai-first-workshop.pdf`** - 3-hour workshop where attendees build from scratch -- **`workshop-prompts.md`** - Prompts for the 3-hour workshop (auto-generated) +- **`ai-first-workshop.pdf`** - 3-hour guided workshop (everyone builds same feature) +- **`workshop-prompts.md`** - Prompts for the guided workshop (auto-generated) +- **`ai-first-openended-workshop.pdf`** - 3-hour open-ended workshop (choose your project) +- **`openended-workshop-prompts.md`** - Prompts for open-ended workshop (auto-generated) - **`claude-code-interactive-tutorial.pdf`** - Original 40+ slide interactive workshop - **`prompts.md`** - Original workshop prompts with page references - **`claude-code-tutorial.pdf`** - 17-slide overview presentation (short talks) @@ -22,7 +25,8 @@ ### Python Scripts - **`presentation_utils.py`** - Shared utilities for PDF generation - **`create_lecture_presentation.py`** - Generates the 1-hour lecture PDF -- **`create_handson_workshop.py`** - Generates the 3-hour workshop PDF + workshop-prompts.md +- **`create_handson_workshop.py`** - Generates the 3-hour guided workshop PDF +- **`create_openended_workshop.py`** - Generates the 3-hour open-ended workshop PDF - **`create_interactive_presentation_v2.py`** - Generates the interactive workshop PDF + prompts.md - **`create_presentation.py`** - Generates the 17-slide overview PDF @@ -110,7 +114,85 @@ They build their OWN project from scratch. --- -## Option 3: Original Interactive Workshop (2-3 hours) +## Option 3: 3-Hour Open-Ended Workshop + +**Use:** `ai-first-openended-workshop.pdf` + `openended-workshop-prompts.md` + +### When to Use +- Hackathons +- Experienced developers +- Teams who want to explore their own ideas +- Groups comfortable with self-directed learning + +### Prerequisites for Attendees +- Laptop with internet +- Claude Code access (or other AI coding assistant) +- Git installed +- GitHub account +- Preferred language runtime (Go, Node, Python, TypeScript) +- Code editor (VS Code, Cursor, etc.) + +### Sample Projects Available + +Attendees can choose from `github.com/emmanuelandre/unveiling-claude`: + +| Project | Description | Complexity | +|---------|-------------|------------| +| **manu-code** | AI-powered CLI code assistant | High | +| **task-manager** | Task management system with API | Medium | +| **my-api-project** | Simple API project starter | Low | +| **prompt-ops** | Prompt operations tooling | Medium | +| **ui-to-test** | UI testing project | Medium | + +Or attendees can propose their **own project idea**. + +### Timeline + +| Part | Duration | Content | Hands-On | +|------|----------|---------|----------| +| Part 1 | 10 min | AI-First philosophy recap | No | +| Part 1 | 15 min | Sample projects overview | No | +| Part 1 | 15 min | Project selection | Exercise 1 | +| Part 1 | 5 min | Setup (fork/create repo) | Exercise 2 | +| Part 2 | 20 min | Planning phase | Exercise 3 | +| Part 2 | 35 min | Implementation sprint 1 | Exercise 4 | +| Break | 10 min | | | +| Part 2 | 25 min | Implementation sprint 2 | Exercise 5 | +| Part 3 | 15 min | Git workflow | Exercise 6 | +| Part 3 | 20 min | Show & Tell demos | No | +| Part 3 | 10 min | Wrap-up | No | + +### Key Exercises +1. Browse & choose your project +2. Initial setup (fork or create repo) +3. Create your plan with AI (CLAUDE.md + spec) +4. Implementation sprint 1 (core feature) +5. Implementation sprint 2 (continue + polish) +6. Commit and push / create PR + +### Key Difference from Guided Workshop + +In the **guided workshop** (Option 2): +- Everyone builds the same authentication feature +- Step-by-step instructions for each part +- Instructor demonstrates, then attendees follow + +In the **open-ended workshop** (Option 3): +- Everyone chooses their own project +- Minimal guidance after project selection +- Attendees work independently with their AI assistant +- Focus on learning through exploration + +### Instructor Tips for Open-Ended Format + +1. **During project selection** - Walk around and help attendees decide +2. **During sprints** - Be available for questions but don't guide too much +3. **Encourage experimentation** - Different approaches are expected +4. **Show & Tell is key** - Let attendees share what they learned + +--- + +## Option 4: Original Interactive Workshop (2-3 hours) **Use:** `claude-code-interactive-tutorial.pdf` + `prompts.md` @@ -226,9 +308,12 @@ source .venv/bin/activate # Generate 1-hour lecture .venv/bin/python3 create_lecture_presentation.py -# Generate 3-hour workshop + prompts +# Generate 3-hour guided workshop + prompts .venv/bin/python3 create_handson_workshop.py +# Generate 3-hour open-ended workshop + prompts +.venv/bin/python3 create_openended_workshop.py + # Generate original interactive workshop + prompts .venv/bin/python3 create_interactive_presentation_v2.py diff --git a/ai-first-openended-workshop.pdf b/ai-first-openended-workshop.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3a3341e44db794f1402581c58fbf21f01050408 GIT binary patch literal 48621 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 EA9@Y#IRF3v literal 0 HcmV?d00001 diff --git a/create_openended_workshop.py b/create_openended_workshop.py new file mode 100644 index 0000000..07840c1 --- /dev/null +++ b/create_openended_workshop.py @@ -0,0 +1,651 @@ +#!/usr/bin/env python3 +""" +Generate 3-hour AI-First Open-Ended Workshop presentation. +Attendees choose a project from unveiling-claude specs or their own idea. +Minimal guidance after project selection - independent work with AI. +""" + +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import Spacer, PageBreak +from reportlab.lib.units import inch +from presentation_utils import ( + create_document, + create_title_slide, + create_section_slide, + create_content_slide, + create_two_column_slide, + create_hands_on_slide, + create_thank_you_slide, + export_prompts_to_markdown, + NumberedCanvas, +) + +# Collect all prompts for export +ALL_PROMPTS = [] + + +def create_presentation(): + """Generate the complete open-ended workshop presentation.""" + doc = create_document("ai-first-openended-workshop.pdf") + styles = getSampleStyleSheet() + story = [] + page_num = 1 + + # ================= + # TITLE + # ================= + create_title_slide( + story, styles, + "AI-First Development", + "Open-Ended Workshop", + "Choose Your Project • Build with AI • 3 Hours" + ) + page_num += 1 + + # ================= + # AGENDA + # ================= + create_content_slide(story, styles, "Workshop Agenda (3 Hours)", [ + "Part 1: Foundation & Project Selection (45 min)", + ("", [ + "AI-First philosophy quick recap", + "Review sample projects from unveiling-claude", + "Choose your project (or bring your own idea)", + "Initial setup" + ]), + "Part 2: Independent Build (90 min)", + ("", [ + "Planning phase with AI", + "Implementation sprints (with break)", + "Work at your own pace" + ]), + "Part 3: Review & Share (45 min)", + ("", [ + "Git workflow - commit and PR", + "Show & Tell - demo your progress", + "Wrap-up and next steps" + ]) + ]) + page_num += 1 + + # ================= + # PART 1: FOUNDATION & PROJECT SELECTION + # ================= + create_section_slide(story, styles, "Part 1: Foundation & Project Selection") + page_num += 1 + + # Quick Recap + create_content_slide(story, styles, "AI-First Philosophy (Quick Recap)", [ + "AI executes 100% → Human validates 100%", + ("Core principle:", [ + "AI handles coding, testing, documentation", + "Human handles validation, review, decisions" + ]), + "You are the architect, AI is the builder", + "Today: You choose what to build!" + ]) + page_num += 1 + + create_content_slide(story, styles, "The 10-Step Workflow (Reference)", [ + "1. Specification (Human writes)", + "2. Database Schema (AI → Human reviews)", + "3. Repository Layer (AI → Human reviews)", + "4. API Endpoints (AI → Human reviews)", + "5. API Tests (AI → Human verifies)", + "6. Frontend (AI → Human reviews)", + "7. UI Tests (AI → Human verifies)", + "8. Documentation (AI → Human reviews)", + "9. Code Review (Human)", + "10. Deployment (AI → Human verifies)", + "Use as much or as little as fits your project" + ]) + page_num += 1 + + create_content_slide(story, styles, "Today's Format", [ + ("Different from guided workshops:", [ + "YOU choose the project", + "YOU decide what to build", + "YOU work at your own pace" + ]), + ("Instructor is here to:", [ + "Help when you're stuck", + "Answer questions", + "Keep time" + ]), + "Goal: Experience real AI-first development" + ]) + page_num += 1 + + # Sample Projects Overview + create_section_slide(story, styles, "Sample Projects Overview") + page_num += 1 + + create_content_slide(story, styles, "Sample Specs: github.com/emmanuelandre/unveiling-claude", [ + "Repository contains several project specifications", + ("Each project has:", [ + "Detailed specifications", + "Architecture documents", + "Implementation guidance" + ]), + "Review the specs before choosing", + "Or: Bring your own project idea!" + ]) + page_num += 1 + + create_content_slide(story, styles, "Option 1: manu-code (High Complexity)", [ + "AI-powered CLI code generation assistant", + ("Features:", [ + "Natural language to code", + "Multi-provider support (Anthropic, OpenAI, Gemini)", + "File operations, shell execution, git integration" + ]), + ("Tech Stack:", [ + "TypeScript / Node.js", + "Commander, Inquirer CLI libraries" + ]), + "Good for: Experienced developers wanting a challenge", + "Spec: manu-code/manu-code-specs.md" + ]) + page_num += 1 + + create_content_slide(story, styles, "Option 2: task-manager (Medium Complexity)", [ + "Task management system with API", + ("Features:", [ + "CRUD operations for tasks", + "User authentication", + "Task assignment and status tracking" + ]), + ("Tech Stack:", [ + "Go or Node.js backend", + "PostgreSQL database" + ]), + "Good for: Learning full-stack API development", + "Spec: task-manager/ directory" + ]) + page_num += 1 + + create_content_slide(story, styles, "Option 3: my-api-project (Low Complexity)", [ + "Simple REST API starter project", + ("Features:", [ + "Basic CRUD endpoints", + "Database migrations", + "Simple authentication" + ]), + ("Tech Stack:", [ + "Go backend", + "PostgreSQL or SQLite" + ]), + "Good for: Beginners or quick projects", + "Spec: my-api-project/ directory" + ]) + page_num += 1 + + create_content_slide(story, styles, "Option 4: Other Projects", [ + ("prompt-ops:", [ + "Prompt operations tooling", + "Medium complexity" + ]), + ("ui-to-test:", [ + "UI testing project", + "Focus on frontend testing" + ]), + ("Bring Your Own Idea:", [ + "Have a project in mind? Build it!", + "Personal tool, side project, experiment", + "Tell your AI assistant what you want to build" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "How to Review a Spec", [ + ("When looking at a spec, check:", [ + "What problem does it solve?", + "What are the core features?", + "What tech stack is suggested?", + "What can I realistically build in 90 minutes?" + ]), + ("Scoping for today:", [ + "Pick ONE feature to implement", + "Focus on core functionality", + "Tests are important but scope them too" + ]) + ]) + page_num += 1 + + # Project Selection Exercise + create_hands_on_slide( + story, styles, + "Exercise 1: Browse & Choose Your Project", +"""Go to: github.com/emmanuelandre/unveiling-claude + +Browse the available projects: +- manu-code/ - AI CLI assistant (High complexity) +- task-manager/ - Task management API (Medium) +- my-api-project/ - Simple API starter (Low) +- prompt-ops/ - Prompt operations (Medium) +- ui-to-test/ - UI testing (Medium) + +Read the specs and choose ONE project. + +OR: Come up with your own idea! + +When ready, raise your hand to share your choice.""", + "You've chosen a project and have a rough idea of what to build", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Decision Checklist", [ + ("Before moving on, confirm:", [ + "☐ I've chosen a project", + "☐ I know which feature(s) to focus on", + "☐ I have a rough idea of the tech stack", + "☐ I'm ready to start!" + ]), + ("If stuck:", [ + "Start with my-api-project (simplest)", + "Ask instructor for guidance", + "Pair with someone on the same project" + ]) + ]) + page_num += 1 + + # Setup + create_hands_on_slide( + story, styles, + "Exercise 2: Initial Setup", +"""Choose ONE option: + +OPTION A: Fork unveiling-claude (if using sample project) +1. Go to github.com/emmanuelandre/unveiling-claude +2. Click "Fork" to create your copy +3. Clone your fork: + git clone https://github.com/[YOUR-USERNAME]/unveiling-claude.git + cd unveiling-claude/[your-project] + +OPTION B: Create new repo (if using own idea) +1. Create new repo on GitHub +2. Clone it locally: + git clone https://github.com/[YOUR-USERNAME]/[your-repo].git + cd [your-repo] + git checkout -b feature/initial-setup + +Open your AI assistant and get ready!""", + "Repository set up and ready to work", + page_num, ALL_PROMPTS) + page_num += 1 + + # ================= + # PART 2: INDEPENDENT BUILD + # ================= + create_section_slide(story, styles, "Part 2: Independent Build (90 min)") + page_num += 1 + + create_content_slide(story, styles, "How This Part Works", [ + ("You will work independently:", [ + "Use your AI assistant as your coding partner", + "Follow the 10-step workflow (as much as applies)", + "Ask instructor if completely stuck" + ]), + ("Timeline:", [ + "Planning Phase: 20 min", + "Implementation Sprint 1: 35 min", + "Break: 10 min", + "Implementation Sprint 2: 25 min" + ]), + "Checkpoints will be announced" + ]) + page_num += 1 + + # Planning Phase + create_section_slide(story, styles, "Planning Phase (20 min)") + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 3: Create Your Plan with AI", +"""Tell your AI assistant: + +I want to build [PROJECT NAME]. + +Project context: [Brief description or link to spec] + +Help me create: +1. A CLAUDE.md (or project rules file) with: + - Project overview + - Tech stack + - Project structure + - Commands (build, test, run) + - Testing requirements + +2. A specification for the FIRST feature I should build + - Keep it scoped for ~60 min implementation + - Include success criteria + +3. A rough implementation plan + +My tech stack preference: [Go/Node/Python/TypeScript]""", + "CLAUDE.md created and first feature specified", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Planning Tips", [ + ("Keep scope realistic:", [ + "One feature, working end-to-end", + "Better to finish small than abandon big" + ]), + ("Be specific with AI:", [ + "Describe what you want clearly", + "Include constraints and preferences", + "Ask for clarification if needed" + ]), + ("Example good scope:", [ + "User registration endpoint with validation", + "Single CRUD resource with tests", + "CLI command that does one thing well" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Checkpoint: Share Your Plan", [ + ("In 2 minutes, be ready to share:", [ + "What project did you choose?", + "What feature are you building?", + "What's your first step?" + ]), + "Quick round-robin (30 seconds each)", + "This helps everyone stay on track" + ]) + page_num += 1 + + # Implementation Guidelines + create_content_slide(story, styles, "Implementation Guidelines", [ + ("Follow the workflow:", [ + "Database/data layer first (if needed)", + "Core logic/API second", + "Tests third", + "Don't skip tests!" + ]), + ("Working with AI:", [ + "Review what AI generates", + "Ask for explanations if unclear", + "Request changes if needed" + ]), + ("When stuck:", [ + "Ask AI to explain the error", + "Try a different approach", + "Ask instructor for help" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Quality Checkpoints", [ + ("After each component, verify:", [ + "☐ Does it compile/run?", + "☐ Does it do what the spec says?", + "☐ Are there obvious bugs?", + "☐ Is the code readable?" + ]), + ("Don't worry about:", [ + "Perfect code on first try", + "Complete feature coverage", + "Production-ready polish" + ]), + "Focus on: Working software you understand" + ]) + page_num += 1 + + # Sprint 1 + create_section_slide(story, styles, "Sprint 1: Build Core Feature (35 min)") + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 4: Implementation Sprint 1", +"""Time to build! Follow your plan. + +Suggested approach: +1. Start with data layer (database schema, models) +2. Then business logic (repository, handlers) +3. Then tests + +Example prompt to start: +"Based on our spec, let's start implementing. +First, create the database schema for [your feature]. +Include proper types, indexes, and constraints." + +Continue from there. Work at your own pace. + +Checkpoint in 35 minutes!""", + "Core feature partially or fully implemented", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Sprint 1 Checkpoint Questions", [ + ("Where are you?", [ + "Database/models done?", + "Core logic started?", + "Any blockers?" + ]), + ("Quick assessment:", [ + "On track → Keep going", + "Behind → Scope down", + "Stuck → Ask for help" + ]), + "No judgment - everyone works at different speeds" + ]) + page_num += 1 + + # Break + create_section_slide(story, styles, "Break (10 min)") + page_num += 1 + + # Sprint 2 + create_section_slide(story, styles, "Sprint 2: Continue Building (25 min)") + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 5: Implementation Sprint 2", +"""Continue building your feature. + +Focus on: +- Completing what you started +- Adding tests for what works +- Making it demo-able + +If you finished early: +- Add another small feature +- Improve tests +- Clean up code +- Help a neighbor + +Remember: Something working > Something perfect + +Checkpoint in 25 minutes - prepare for demo!""", + "Feature implemented and ready to demo", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "Prepare for Demo", [ + ("In your demo, plan to show:", [ + "What you built (quick walkthrough)", + "One working feature in action", + "What you learned" + ]), + ("Keep it short:", [ + "2-3 minutes max per person", + "Focus on the interesting parts", + "It's OK if it's not finished!" + ]) + ]) + page_num += 1 + + # ================= + # PART 3: REVIEW & SHARE + # ================= + create_section_slide(story, styles, "Part 3: Review & Share") + page_num += 1 + + # Git Workflow + create_content_slide(story, styles, "Git Workflow", [ + ("Before demo, commit your work:", [ + "Stage changes: git add .", + "Review: git status", + "Commit with conventional format" + ]), + ("Commit message format:", [ + "feat(scope): description", + "Example: feat(auth): add user registration endpoint" + ]), + "Push to your fork/repo" + ]) + page_num += 1 + + create_hands_on_slide( + story, styles, + "Exercise 6: Commit and Push", +"""Commit and push your work: + +1. Review changes: + git status + git diff + +2. Stage and commit: + git add . + git commit -m "feat([scope]): [what you built] + + - [Key thing 1] + - [Key thing 2] + - [Key thing 3]" + +3. Push: + git push origin [your-branch] + +4. (Optional) Create PR: + gh pr create --title "feat: [your feature]" \\ + --body "## What I Built + [Description] + + ## What Works + - [Feature 1] + - [Feature 2]" + +Or create PR through GitHub web interface.""", + "Code committed and pushed to GitHub", + page_num, ALL_PROMPTS) + page_num += 1 + + create_content_slide(story, styles, "PR Description Template", [ + ("Include:", [ + "## What I Built - brief description", + "## What Works - list working features", + "## What's Next - future improvements", + "## Lessons Learned - optional but valuable" + ]), + "This documents your workshop experience", + "Great reference for continuing at home" + ]) + page_num += 1 + + # Show & Tell + create_section_slide(story, styles, "Show & Tell (20 min)") + page_num += 1 + + create_content_slide(story, styles, "Demo Format", [ + ("Each person: 2-3 minutes", [ + "What project did you choose?", + "What did you build?", + "Quick demo of one thing working", + "What was interesting/challenging?" + ]), + ("It's OK to show:", [ + "Partially working features", + "Interesting failures", + "What you learned from errors" + ]), + "Celebrate progress, not perfection!" + ]) + page_num += 1 + + create_content_slide(story, styles, "While Others Demo", [ + ("Listen for:", [ + "Interesting approaches", + "Useful AI prompts", + "Problems you also faced" + ]), + "Save questions for after all demos", + "Note ideas for your own projects" + ]) + page_num += 1 + + # Wrap-up + create_section_slide(story, styles, "Wrap-Up") + page_num += 1 + + create_content_slide(story, styles, "Key Learnings", [ + ("Today you experienced:", [ + "Choosing and scoping a project", + "Planning with AI assistance", + "Independent implementation", + "Real development workflow" + ]), + ("The AI-First approach:", [ + "AI helps you move faster", + "You make the decisions", + "Review everything", + "Tests matter" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Continue at Home", [ + ("Your project isn't finished - keep going!", [ + "Complete the feature you started", + "Add more tests", + "Try another feature from the spec" + ]), + ("Tips for solo work:", [ + "Commit frequently", + "Take breaks", + "Review AI output carefully", + "Document what works" + ]) + ]) + page_num += 1 + + create_content_slide(story, styles, "Resources", [ + ("Sample Specs:", [ + "github.com/emmanuelandre/unveiling-claude" + ]), + ("AI Tools:", [ + "Claude Code: claude.ai/code", + "Windsurf: codeium.com/windsurf", + "Cursor: cursor.sh" + ]), + ("Documentation:", [ + "See /docs folder for detailed guides", + "CLAUDE.md templates in /examples" + ]) + ]) + page_num += 1 + + # ================= + # THANK YOU + # ================= + create_thank_you_slide(story, styles, "Thank You!", "Keep Building!") + + # Build PDF + doc.build(story, canvasmaker=NumberedCanvas) + print("✅ Open-ended workshop created: ai-first-openended-workshop.pdf") + + # Export prompts + export_prompts_to_markdown( + ALL_PROMPTS, + "openended-workshop-prompts.md", + "AI-First Open-Ended Workshop Prompts" + ) + print("✅ Prompts exported: openended-workshop-prompts.md") + + +if __name__ == "__main__": + create_presentation() diff --git a/openended-workshop-prompts.md b/openended-workshop-prompts.md new file mode 100644 index 0000000..b2dab87 --- /dev/null +++ b/openended-workshop-prompts.md @@ -0,0 +1,182 @@ +# AI-First Open-Ended Workshop Prompts + +Copy and paste these prompts during the workshop exercises. + +--- + +## Page 14: Exercise 1: Browse & Choose Your Project + +**PROMPT:** +``` +Go to: github.com/emmanuelandre/unveiling-claude + +Browse the available projects: +- manu-code/ - AI CLI assistant (High complexity) +- task-manager/ - Task management API (Medium) +- my-api-project/ - Simple API starter (Low) +- prompt-ops/ - Prompt operations (Medium) +- ui-to-test/ - UI testing (Medium) + +Read the specs and choose ONE project. + +OR: Come up with your own idea! + +When ready, raise your hand to share your choice. +``` + +**EXPECTED RESULT:** +You've chosen a project and have a rough idea of what to build + +--- + +## Page 16: Exercise 2: Initial Setup + +**PROMPT:** +``` +Choose ONE option: + +OPTION A: Fork unveiling-claude (if using sample project) +1. Go to github.com/emmanuelandre/unveiling-claude +2. Click "Fork" to create your copy +3. Clone your fork: + git clone https://github.com/[YOUR-USERNAME]/unveiling-claude.git + cd unveiling-claude/[your-project] + +OPTION B: Create new repo (if using own idea) +1. Create new repo on GitHub +2. Clone it locally: + git clone https://github.com/[YOUR-USERNAME]/[your-repo].git + cd [your-repo] + git checkout -b feature/initial-setup + +Open your AI assistant and get ready! +``` + +**EXPECTED RESULT:** +Repository set up and ready to work + +--- + +## Page 20: Exercise 3: Create Your Plan with AI + +**PROMPT:** +``` +Tell your AI assistant: + +I want to build [PROJECT NAME]. + +Project context: [Brief description or link to spec] + +Help me create: +1. A CLAUDE.md (or project rules file) with: + - Project overview + - Tech stack + - Project structure + - Commands (build, test, run) + - Testing requirements + +2. A specification for the FIRST feature I should build + - Keep it scoped for ~60 min implementation + - Include success criteria + +3. A rough implementation plan + +My tech stack preference: [Go/Node/Python/TypeScript] +``` + +**EXPECTED RESULT:** +CLAUDE.md created and first feature specified + +--- + +## Page 26: Exercise 4: Implementation Sprint 1 + +**PROMPT:** +``` +Time to build! Follow your plan. + +Suggested approach: +1. Start with data layer (database schema, models) +2. Then business logic (repository, handlers) +3. Then tests + +Example prompt to start: +"Based on our spec, let's start implementing. +First, create the database schema for [your feature]. +Include proper types, indexes, and constraints." + +Continue from there. Work at your own pace. + +Checkpoint in 35 minutes! +``` + +**EXPECTED RESULT:** +Core feature partially or fully implemented + +--- + +## Page 30: Exercise 5: Implementation Sprint 2 + +**PROMPT:** +``` +Continue building your feature. + +Focus on: +- Completing what you started +- Adding tests for what works +- Making it demo-able + +If you finished early: +- Add another small feature +- Improve tests +- Clean up code +- Help a neighbor + +Remember: Something working > Something perfect + +Checkpoint in 25 minutes - prepare for demo! +``` + +**EXPECTED RESULT:** +Feature implemented and ready to demo + +--- + +## Page 34: Exercise 6: Commit and Push + +**PROMPT:** +``` +Commit and push your work: + +1. Review changes: + git status + git diff + +2. Stage and commit: + git add . + git commit -m "feat([scope]): [what you built] + + - [Key thing 1] + - [Key thing 2] + - [Key thing 3]" + +3. Push: + git push origin [your-branch] + +4. (Optional) Create PR: + gh pr create --title "feat: [your feature]" \ + --body "## What I Built + [Description] + + ## What Works + - [Feature 1] + - [Feature 2]" + +Or create PR through GitHub web interface. +``` + +**EXPECTED RESULT:** +Code committed and pushed to GitHub + +--- +

ZKLNDFjlE!j$Lk0GVQk@O)^ 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 C63kTq diff --git a/create_interactive_presentation_v2.py b/create_interactive_presentation_v2.py index 502d5b9..0a1505f 100644 --- a/create_interactive_presentation_v2.py +++ b/create_interactive_presentation_v2.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ -Generate comprehensive interactive Claude Code tutorial presentation +Generate comprehensive interactive AI-First Development tutorial presentation +Supports Claude Code and Windsurf IDE All examples use React (frontend) and Go (backend) Properly formatted prompts with line breaks """ @@ -61,16 +62,16 @@ def create_title_slide(story, styles): """Title slide""" story.append(Spacer(1, 1.5*inch)) - title = Paragraph('Claude Code', styles['Title']) + title = Paragraph('AI-First Development', styles['Title']) story.append(title) story.append(Spacer(1, 0.2*inch)) - subtitle = Paragraph('Interactive Tutorial & Workshop', styles['Title']) + subtitle = Paragraph('Interactive Tutorial & Workshop', styles['Title']) story.append(subtitle) - story.append(Spacer(1, 0.3*inch)) + story.append(Spacer(1, 0.2*inch)) - desc = Paragraph('AI-First Development for Modern Software Teams', styles['Title']) - story.append(desc) + tools = Paragraph('Using Claude Code, Windsurf, and AI Coding Assistants', styles['Title']) + story.append(tools) story.append(PageBreak()) @@ -343,16 +344,13 @@ def create_presentation(): create_content_slide(story, styles, "Testing Philosophy", [ "E2E tests are MANDATORY (API + UI user journeys)", - "Unit tests are MANDATORY (business logic, utilities, edge cases)", - "Component tests are GOOD TO HAVE (test containers for microservices)", + "Unit tests are MANDATORY (business logic, edge cases)", + "Component tests are GOOD TO HAVE (microservices)", ("Test-First Approach:", [ "Define coverage targets before implementation", - "Build testing infrastructure/framework first", - "Track coverage from unit, component, and E2E tests", - "Coverage thresholds vary by project (higher is better)", + "Build testing infrastructure first", "Tests are your regression safety net" - ]), - "Measure coverage to ensure quality and confidence" + ]) ]) page_num += 1 @@ -391,45 +389,57 @@ def create_presentation(): create_content_slide(story, styles, "Concern #2: Security Risks", [ ("The Concern:", [ - "AI might introduce vulnerabilities (SQL injection, weak crypto)", - "Proprietary code exposed to cloud-based AI tools", - "Unknown dependencies with security issues" + "AI might introduce vulnerabilities", + "Proprietary code exposed to AI tools" ]), ("How We Address It:", [ - "Security checklist in code review (see docs/15-security.md)", - "Automated security scanning in CI (gosec, npm audit)", - "Claude Code runs locally - your code stays on your machine", + "Security checklist in code review", + "Automated security scanning in CI", "Explicit prompts for secure patterns" ]) ]) page_num += 1 + create_content_slide(story, styles, "How Your Data is Handled", [ + ("Your code stays on your machine:", [ + "AI assistants run locally", + "Files are read/written locally" + ]), + ("When you ask for help:", [ + "Relevant code sent to AI API as context", + "Processed but NOT stored on AI servers", + "Similar to pasting code in chat" + ]), + ("Different from cloud IDEs:", [ + "Cloud IDEs store your code remotely", + "AI assistants only send context when asked" + ]) + ]) + page_num += 1 + create_content_slide(story, styles, "Concern #3: Maintainability & Technical Debt", [ ("The Concern:", [ - "AI-generated code hard to understand or maintain", - "Suggestions may not follow team conventions", - "Quick fixes pile up without proper review" + "AI-generated code hard to understand/maintain", + "Suggestions may not follow team conventions" ]), ("How We Address It:", [ - "CLAUDE.md defines all conventions - AI follows them", - "Consistent patterns across entire codebase", - "Human review catches non-idiomatic code", - "Refactor requests explicit in prompts" + "Project rules (CLAUDE.md / Windsurf Rules) define conventions", + "Consistent patterns across codebase", + "Human review catches non-idiomatic code" ]) ]) page_num += 1 create_content_slide(story, styles, "Concern #4: Design Integrity", [ ("The Concern:", [ - "AI optimizes for local solutions, not holistic design", - "Code may work but ignore design principles (SOLID)", - "Risk of skipping critical design thinking" + "AI optimizes locally, not holistically", + "May ignore design principles (SOLID)" ]), ("How We Address It:", [ - "Architecture documented in CLAUDE.md", - "Human writes specifications BEFORE AI implements", - "Design decisions in ADRs (Architecture Decision Records)", - "AI follows existing patterns, doesn't invent new ones" + "Architecture in project rules (CLAUDE.md / Windsurf Rules)", + "Human writes specs BEFORE AI implements", + "Design decisions in ADRs", + "AI follows existing patterns" ]) ]) page_num += 1 @@ -473,8 +483,7 @@ def create_presentation(): "Systematic validation at every step", "Comprehensive testing (E2E + Unit)", "Clear human ownership and accountability", - "Documented conventions in CLAUDE.md", - "Quality gates that must pass before merge" + "Documented conventions in project rules" ]), "Result: Faster development WITH maintained quality" ]) @@ -487,33 +496,30 @@ def create_presentation(): page_num += 1 create_content_slide(story, styles, "What is Vibe Coding?", [ - "Definition: Informal, exploratory approach with minimal instructions", + "Informal, exploratory approach with minimal instructions", ("Characteristics:", [ - "Speed and experimentation over precision", - "Relies on AI to 'guess' or 'fill in' intent", - "Minimal context provided" + "Speed over precision", + "AI 'guesses' intent", + "Minimal context" ]), "Example: 'Make something that sorts numbers'", ("Result:", [ - "May work quickly for prototypes", - "Unpredictable or suboptimal code", - "Hard to maintain and debug" + "Quick for prototypes", + "Unpredictable code quality" ]) ]) page_num += 1 create_content_slide(story, styles, "What is Prompt Engineering?", [ - "Definition: Precise, structured inputs to guide AI behavior", + "Precise, structured inputs to guide AI behavior", ("Characteristics:", [ - "Context, constraints, and examples provided", - "Understanding how the model interprets language", - "Clear success criteria defined" + "Context and constraints provided", + "Clear success criteria" ]), - "Example: 'Write a Python function to sort integers ascending, no built-in sort'", + "Example: 'Write Python function to sort integers ascending, no built-in sort'", ("Result:", [ - "Predictable, production-quality code", - "Easier to maintain and extend", - "Consistent with project standards" + "Predictable, production code", + "Consistent with standards" ]) ]) page_num += 1 @@ -521,35 +527,28 @@ def create_presentation(): create_content_slide(story, styles, "Side-by-Side Comparison", [ ("Speed:", [ "Vibe: Fast initial results", - "Prompt: Efficient overall (less rework)" + "Prompt: Less rework overall" ]), ("Quality:", [ - "Vibe: Unpredictable, may need fixes", - "Prompt: Consistent, meets requirements" - ]), - ("Maintenance:", [ - "Vibe: Harder - unclear intent", - "Prompt: Easier - documented approach" + "Vibe: Unpredictable", + "Prompt: Consistent" ]), ("Best For:", [ - "Vibe: Quick prototypes, exploration", - "Prompt: Production code, team projects" + "Vibe: Prototypes", + "Prompt: Production code" ]) ]) page_num += 1 create_content_slide(story, styles, "This Workshop Uses Prompt Engineering", [ - "We teach structured, production-quality prompting", + "Structured, production-quality prompting", ("The AI-First workflow:", [ - "Human writes detailed specification", - "AI executes within defined boundaries", - "Human validates every output" - ]), - ("Flow: Spec → Schema → Code → Tests", [ - "Each step has clear inputs and outputs", - "Nothing left to 'vibes' or guesswork" + "Human writes specification", + "AI executes within boundaries", + "Human validates output" ]), - "Result: Code you can confidently ship to production" + "Flow: Spec → Schema → Code → Tests", + "Result: Code you can ship to production" ]) page_num += 1 @@ -1092,30 +1091,28 @@ def create_presentation(): page_num += 1 create_content_slide(story, styles, "Keys to Success", [ - ("1. CLAUDE.md is essential", [ - "Keep it current and comprehensive", - "Document all conventions and patterns" + ("1. Project rules are essential", [ + "CLAUDE.md / Windsurf Rules - keep current", + "Document conventions and patterns" ]), ("2. Clear handoffs between AI and human", [ - "AI completes a step fully before handoff", + "AI completes step fully before handoff", "Human approves or requests changes" ]), - ("3. Systematic quality gates", [ - "Tests must pass before proceeding", - "Reviews must approve before merging" + ("3. Quality gates", [ + "Tests pass before proceeding", + "Reviews approve before merging" ]) ]) page_num += 1 create_content_slide(story, styles, "Common Mistakes to Avoid", [ - "❌ Vague prompts → Be specific with examples", - "❌ Skipping tests → Always write E2E tests first", - "❌ Committing untested code → Run checks locally", - "❌ Ignoring CLAUDE.md → Keep it updated", - "❌ Large unfocused PRs → Make small, atomic changes", - "✅ Review all AI output before committing", - "✅ Use conventional commits", - "✅ Run pre-commit checks" + "❌ Vague prompts → Be specific", + "❌ Skipping tests → Write E2E tests first", + "❌ Untested commits → Run checks locally", + "❌ Ignoring project rules → Keep them updated", + "❌ Large PRs → Make atomic changes", + "✅ Review AI output before committing" ]) page_num += 1 @@ -1153,15 +1150,13 @@ def create_presentation(): create_content_slide(story, styles, "The Challenge of Large Projects", [ ("When projects grow:", [ - "100+ tasks across multiple modules", - "Complex dependencies between features", - "Weeks or months of development", - "Context loss between sessions", - "Need systematic progress tracking" + "100+ tasks across modules", + "Complex dependencies", + "Context loss between sessions" ]), ("The Solution:", [ - "Phased development with living documentation", - "Centralized project planning structure", + "Phased development", + "Centralized planning structure", "Continuous progress monitoring" ]) ]) @@ -1170,47 +1165,40 @@ def create_presentation(): create_content_slide(story, styles, "Project Planning Structure", [ ("Directory Layout:", [ "project/planning/ - Master plan and progress", - "project/specs/ - Detailed feature specifications", - "project/sessions/ - Session summaries", - "project/development/ - Quick reference guides" + "project/specs/ - Feature specifications", + "project/sessions/ - Session summaries" ]), ("Key Files:", [ - "devplan.md - Master plan with all phases", - "devprogress.md - Living progress tracker", - "database.md - Complete schema documentation", - "Session notes - What happened each session" + "devplan.md - Master plan with phases", + "devprogress.md - Progress tracker", + "database.md - Schema documentation" ]) ]) page_num += 1 create_content_slide(story, styles, "The Master Plan (devplan.md)", [ ("Contains:", [ - "10 phases with clear objectives", - "Dependency mapping (what depends on what)", - "Vertical slice workflow per feature", - "All tasks with checkboxes", - "Estimated timelines" + "Phases with clear objectives", + "Dependency mapping", + "Tasks with checkboxes" ]), ("Workflow per Feature:", [ - "DB → Backend API → API Tests → UI → UI Tests → Docs", - "Complete each layer before moving forward", - "Never skip the testing phases" + "DB → API → Tests → UI → UI Tests", + "Complete each layer before moving", + "Never skip testing" ]) ]) page_num += 1 create_content_slide(story, styles, "Progress Tracker (devprogress.md)", [ ("Update After Every Session:", [ - "Mark completed tasks with [x]", + "Mark completed tasks [x]", "Update phase percentages", - "Track current sprint goals", - "Document blockers and decisions", - "Calculate overall progress" + "Document blockers" ]), ("Quick Stats Table:", [ - "Phase | Status | Progress | Backend | UI | Tests", - "🔴 Not Started | 🟡 In Progress | 🟢 Complete", - "Visual overview of project health" + "Phase | Status | Progress", + "🔴 Not Started | 🟡 In Progress | 🟢 Complete" ]) ]) page_num += 1 @@ -1365,38 +1353,30 @@ def create_presentation(): create_content_slide(story, styles, "Best Practices for Large Projects", [ ("Update Progress Religiously:", [ - "After every session - mark completed tasks", - "Calculate percentages accurately", - "Document blockers immediately", - "Create session notes with decisions" + "Mark tasks after every session", + "Document blockers immediately" ]), ("Keep Plans vs Reality Aligned:", [ - "devplan.md = original blueprint (stable)", - "devprogress.md = current reality (dynamic)", - "Adjust plan when reality diverges significantly" + "devplan.md = blueprint (stable)", + "devprogress.md = reality (dynamic)" ]), ("Use Phase-Based Branches:", [ - "phase-0-foundation, phase-1-projects, etc.", - "Complete entire phase before merging", - "Easier to track and review large changes" + "phase-0-foundation, phase-1-projects", + "Complete phase before merging" ]) ]) page_num += 1 create_content_slide(story, styles, "When to Use This Approach", [ ("Small Projects (<20 tasks):", [ - "❌ Don't need planning structure", - "✅ Simple CLAUDE.md is enough" + "Simple project rules file is enough" ]), ("Medium Projects (20-50 tasks):", [ - "✅ Create devplan.md and devprogress.md", - "✅ Update progress after sessions" + "Create devplan.md and devprogress.md" ]), ("Large Projects (50+ tasks):", [ - "✅ Full planning structure", - "✅ Daily progress updates", - "✅ Session notes after every session", - "✅ Detailed specs for complex features" + "Full planning structure", + "Session notes after every session" ]) ]) page_num += 1 @@ -1408,28 +1388,25 @@ def create_presentation(): page_num += 1 create_content_slide(story, styles, "What We Learned", [ - "✓ AI-First philosophy: AI executes, Human validates", + "✓ AI-First: AI executes, Human validates", "✓ 10-step systematic development process", "✓ E2E tests as primary testing strategy", - "✓ Proper git workflow and conventional commits", - "✓ Effective prompt engineering techniques", - "✓ CLAUDE.md as project instruction manual", - "✓ Scaling to large projects with phased planning", - "✓ Progress tracking with devplan.md and devprogress.md", - "✓ Quality gates at every step" + "✓ Git workflow and conventional commits", + "✓ Prompt engineering techniques", + "✓ Project rules (CLAUDE.md / Windsurf Rules)", + "✓ Phased planning for large projects" ]) page_num += 1 create_content_slide(story, styles, "Your Action Plan", [ ("Next Steps:", [ - "1. Create CLAUDE.md for your project", - "2. Set up git workflow (branches, conventional commits)", + "1. Create project rules file (CLAUDE.md / Windsurf Rules)", + "2. Set up git workflow", "3. Start with one feature using 10-step process", - "4. Write E2E tests for everything", - "5. Review and iterate" + "4. Write E2E tests for everything" ]), ("Resources:", [ - "Claude Code: claude.ai/code", + "AI Assistants: Claude Code, Windsurf", "Prompts: See prompts.md file" ]) ]) diff --git a/prompts.md b/prompts.md index a973a95..2a7133d 100644 --- a/prompts.md +++ b/prompts.md @@ -4,7 +4,7 @@ Copy and paste these prompts during the workshop exercises. --- -## Page 23: Create Your First Project +## Page 24: Create Your First Project **PROMPT:** ``` @@ -27,7 +27,7 @@ Claude creates a comprehensive CLAUDE.md with architecture, commands, and conven --- -## Page 24: Initialize Git Workflow +## Page 25: Initialize Git Workflow **PROMPT:** ``` @@ -44,7 +44,7 @@ Git repository with proper branch structure and commit conventions --- -## Page 27: Step 1 - Write Specification +## Page 28: Step 1 - Write Specification **PROMPT:** ``` @@ -74,7 +74,7 @@ Claude asks clarifying questions and confirms the specification --- -## Page 29: Step 2 - Review Schema +## Page 30: Step 2 - Review Schema **PROMPT:** ``` @@ -97,7 +97,7 @@ Database table created successfully with proper indexes --- -## Page 31: Step 4 - Request API Implementation +## Page 32: Step 4 - Request API Implementation **PROMPT:** ``` @@ -127,7 +127,7 @@ Claude creates handler files with validation and error handling --- -## Page 33: Step 5 - Request API Tests +## Page 34: Step 5 - Request API Tests **PROMPT:** ``` @@ -152,7 +152,7 @@ Cypress test file created with all test cases --- -## Page 35: Run API Tests +## Page 36: Run API Tests **PROMPT:** ``` @@ -171,7 +171,7 @@ All API tests pass (green checkmarks in terminal) --- -## Page 36: Step 6 - Request Frontend +## Page 37: Step 6 - Request Frontend **PROMPT:** ``` @@ -198,7 +198,7 @@ React components created with forms and state management --- -## Page 38: Step 7 - Request UI Tests +## Page 39: Step 7 - Request UI Tests **PROMPT:** ``` @@ -220,7 +220,7 @@ Cypress UI test file created with user journey tests --- -## Page 42: Practice: Write E2E Test +## Page 43: Practice: Write E2E Test **PROMPT:** ``` @@ -242,7 +242,7 @@ Complete password reset feature with passing E2E tests --- -## Page 46: Practice: Proper Git Workflow +## Page 47: Practice: Proper Git Workflow **PROMPT:** ``` @@ -267,7 +267,7 @@ PR created with passing CI checks and proper commit messages --- -## Page 48: Practice: Better Prompts +## Page 49: Practice: Better Prompts **PROMPT:** ``` @@ -296,7 +296,7 @@ You create a comprehensive, specific prompt with clear requirements --- -## Page 52: Final Exercise: Complete Feature +## Page 53: Final Exercise: Complete Feature **PROMPT:** ``` @@ -328,7 +328,7 @@ Complete profile feature with passing tests and PR ready for review --- -## Page 58: EXERCISE: Multi-Phase Task Manager (Phase 0) +## Page 59: EXERCISE: Multi-Phase Task Manager (Phase 0) **PROMPT:** ``` @@ -371,7 +371,7 @@ devplan.md created with 4 phases and dependency mapping --- -## Page 59: EXERCISE: Create Progress Tracker +## Page 60: EXERCISE: Create Progress Tracker **PROMPT:** ``` @@ -406,7 +406,7 @@ devprogress.md and database.md created --- -## Page 60: EXERCISE: Implement Phase 0 Database +## Page 61: EXERCISE: Implement Phase 0 Database **PROMPT:** ``` @@ -444,7 +444,7 @@ Migrations created and progress updated --- -## Page 61: EXERCISE: Implement Auth System +## Page 62: EXERCISE: Implement Auth System **PROMPT:** ``` From 7311708b112a56f76a6ddb2a5ccd7b7e040b6755 Mon Sep 17 00:00:00 2001 From: Emmanuel Andre Date: Thu, 4 Dec 2025 20:27:21 +0800 Subject: [PATCH 3/4] feat: add lecture and hands-on workshop presentations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 1-hour lecture presentation (ai-first-lecture.pdf) - No hands-on, practitioner-level depth - Covers: philosophy, concerns, prompt engineering, testing, git, tips - Tool-agnostic with Claude Code/Windsurf as examples - Add 3-hour hands-on workshop (ai-first-workshop.pdf) - Attendees build from scratch using unveiling-claude as reference - 13 exercises covering full development workflow - Auto-generated workshop-prompts.md - Add presentation_utils.py for shared PDF generation utilities - Update CLAUDE.md with new file structure - Update WORKSHOP_GUIDE.md with all presentation options 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 69 ++- WORKSHOP_GUIDE.md | 132 ++++- ai-first-lecture.pdf | Bin 0 -> 59768 bytes ai-first-workshop.pdf | Bin 0 -> 64550 bytes create_handson_workshop.py | 899 +++++++++++++++++++++++++++++++++ create_lecture_presentation.py | 742 +++++++++++++++++++++++++++ presentation_utils.py | 283 +++++++++++ workshop-prompts.md | 451 +++++++++++++++++ 8 files changed, 2555 insertions(+), 21 deletions(-) create mode 100644 ai-first-lecture.pdf create mode 100644 ai-first-workshop.pdf create mode 100644 create_handson_workshop.py create mode 100644 create_lecture_presentation.py create mode 100644 presentation_utils.py create mode 100644 workshop-prompts.md diff --git a/CLAUDE.md b/CLAUDE.md index a0c73de..9e691bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,10 +38,21 @@ claude-tutorial/ ├── examples/ │ └── claude-md-template.md # Production-ready CLAUDE.md template │ -├── prompts.md # Workshop exercise prompts +├── prompts.md # Original workshop exercise prompts +├── workshop-prompts.md # Hands-on workshop prompts (auto-generated) ├── WORKSHOP_GUIDE.md # Instructor guide for workshops -├── claude-code-interactive-tutorial.pdf # Workshop slides -└── create_*.py # Python scripts to generate presentations +│ +├── # Presentation PDFs +├── claude-code-interactive-tutorial.pdf # Original interactive workshop (2-3 hours) +├── ai-first-lecture.pdf # 1-hour lecture (no hands-on) +├── ai-first-workshop.pdf # 3-hour hands-on workshop +│ +├── # Python scripts to generate presentations +├── presentation_utils.py # Shared utilities for PDF generation +├── create_presentation.py # Generates overview PDF +├── create_interactive_presentation_v2.py # Generates interactive tutorial PDF +├── create_lecture_presentation.py # Generates 1-hour lecture PDF +└── create_handson_workshop.py # Generates 3-hour workshop PDF + prompts ``` ## Key Concepts (Important for Editing) @@ -122,10 +133,28 @@ The `examples/claude-md-template.md` is the **most important file** in this repo ### Workshop Materials -The workshop materials work together as a system: -- `claude-code-interactive-tutorial.pdf` - Main presentation slides -- `prompts.md` - Copy-paste prompts for hands-on exercises (references slide numbers) -- `WORKSHOP_GUIDE.md` - Instructor notes for running workshops +The workshop materials work together as a system. There are three presentation options: + +**1. Original Interactive Workshop (2-3 hours)** +- `claude-code-interactive-tutorial.pdf` - Interactive tutorial with hands-on exercises +- `prompts.md` - Copy-paste prompts (references slide numbers) +- Best for: Standard workshop sessions + +**2. Lecture Presentation (1 hour, no hands-on)** +- `ai-first-lecture.pdf` - Comprehensive lecture covering all topics +- No accompanying prompts file (lecture format) +- Best for: Conference talks, team briefings, executive overviews +- Topics: Philosophy, concerns, prompt engineering, testing, git, tips + +**3. Hands-On Workshop (3 hours)** +- `ai-first-workshop.pdf` - Focused on building from scratch +- `workshop-prompts.md` - Copy-paste prompts (auto-generated) +- Reference repo: `github.com/emmanuelandre/unveiling-claude` +- Best for: Full training sessions, bootcamps +- Attendees build their own project using reference examples + +**Instructor Guide:** +- `WORKSHOP_GUIDE.md` - Notes for running any workshop format **Workshop GitHub Repository Convention:** - All attendees create a private GitHub repository named: `unveiling-claude` @@ -144,9 +173,22 @@ The workshop materials work together as a system: ### Python Presentation Scripts The `create_*.py` files generate presentation PDFs: -- Not part of the tutorial content -- Used to regenerate PDFs when updating slides -- No need to maintain unless updating presentations + +| Script | Output | Purpose | +|--------|--------|---------| +| `presentation_utils.py` | (shared module) | Common utilities for all presentations | +| `create_presentation.py` | `claude-code-tutorial.pdf` | Simple 17-slide overview | +| `create_interactive_presentation_v2.py` | `claude-code-interactive-tutorial.pdf` | Original interactive workshop | +| `create_lecture_presentation.py` | `ai-first-lecture.pdf` | 1-hour lecture (no hands-on) | +| `create_handson_workshop.py` | `ai-first-workshop.pdf` + `workshop-prompts.md` | 3-hour hands-on workshop | + +**To regenerate presentations:** +```bash +.venv/bin/python3 create_lecture_presentation.py # Lecture +.venv/bin/python3 create_handson_workshop.py # Workshop +``` + +**Dependencies:** `reportlab` (installed in `.venv`) ## Common Tasks @@ -224,7 +266,10 @@ fix: correct code example in git workflow | `docs/06-testing-strategy.md` | E2E-first testing philosophy | Core methodology | | `docs/07-ai-first-workflow.md` | 10-step development process | Core methodology | | `docs/11-git-workflow.md` | Git conventions and best practices | Referenced by template | -| `prompts.md` | Workshop exercise prompts | Used with PDF workshop | +| `prompts.md` | Original workshop exercise prompts | Used with interactive tutorial PDF | +| `workshop-prompts.md` | Hands-on workshop prompts | Auto-generated, used with 3-hour workshop | +| `ai-first-lecture.pdf` | 1-hour lecture slides | No hands-on, conference/briefing format | +| `ai-first-workshop.pdf` | 3-hour workshop slides | Hands-on, build from scratch format | ## Important Notes @@ -254,7 +299,7 @@ When making changes, preserve these core principles taught in this tutorial: --- -**Last Updated**: 2025-11-16 +**Last Updated**: 2025-12-04 **Repository Type**: Educational Documentation **Primary Audience**: Software developers learning AI-first development with Claude Code - always update /docs, the pdf slides and the prompts.md diff --git a/WORKSHOP_GUIDE.md b/WORKSHOP_GUIDE.md index e2fbd92..db2e166 100644 --- a/WORKSHOP_GUIDE.md +++ b/WORKSHOP_GUIDE.md @@ -1,15 +1,120 @@ # Workshop Presentation Guide +## Available Presentations + +| Presentation | Duration | Format | Use Case | +|--------------|----------|--------|----------| +| `ai-first-lecture.pdf` | 1 hour | Lecture (no hands-on) | Conference talks, team briefings | +| `ai-first-workshop.pdf` | 3 hours | Hands-on | Training sessions, bootcamps | +| `claude-code-interactive-tutorial.pdf` | 2-3 hours | Interactive | Standard workshops | +| `claude-code-tutorial.pdf` | 30 min | Overview | Quick introductions | + ## Files Overview ### Presentation Files -- **`claude-code-tutorial.pdf`** - 17-slide overview presentation (use for short talks) -- **`claude-code-interactive-tutorial.pdf`** - 40+ slide interactive workshop (full hands-on session) -- **`prompts.md`** - All workshop prompts with page references for easy copy-paste +- **`ai-first-lecture.pdf`** - 1-hour lecture covering all AI-first topics (NO hands-on) +- **`ai-first-workshop.pdf`** - 3-hour workshop where attendees build from scratch +- **`workshop-prompts.md`** - Prompts for the 3-hour workshop (auto-generated) +- **`claude-code-interactive-tutorial.pdf`** - Original 40+ slide interactive workshop +- **`prompts.md`** - Original workshop prompts with page references +- **`claude-code-tutorial.pdf`** - 17-slide overview presentation (short talks) ### Python Scripts +- **`presentation_utils.py`** - Shared utilities for PDF generation +- **`create_lecture_presentation.py`** - Generates the 1-hour lecture PDF +- **`create_handson_workshop.py`** - Generates the 3-hour workshop PDF + workshop-prompts.md +- **`create_interactive_presentation_v2.py`** - Generates the interactive workshop PDF + prompts.md - **`create_presentation.py`** - Generates the 17-slide overview PDF -- **`create_interactive_presentation_v2.py`** - Generates the interactive workshop PDF and prompts.md + +--- + +## Option 1: 1-Hour Lecture (No Hands-On) + +**Use:** `ai-first-lecture.pdf` + +### When to Use +- Conference talks +- Team briefings +- Executive presentations +- Introduction sessions without setup time + +### Topics Covered (~50 min + Q&A) +1. AI-First Philosophy (4 min) +2. Addressing Common Concerns (5 min) +3. Prompt Engineering vs Vibe Coding (4 min) +4. Getting Started with Prompt Engineering (6 min) +5. Scaling to Large Projects (5 min) +6. Feature Documentation Best Practices (5 min) +7. Testing Strategies (5 min) +8. Project Planning & 10-Step Workflow (4 min) +9. Git Best Practices (4 min) +10. Tips & Tricks (5 min) + +### Setup +- Open `ai-first-lecture.pdf` on projector +- No attendee setup needed +- Allow 10-15 min Q&A at end + +--- + +## Option 2: 3-Hour Hands-On Workshop + +**Use:** `ai-first-workshop.pdf` + `workshop-prompts.md` + +### When to Use +- Full training sessions +- Bootcamps +- Teams wanting practical experience +- Groups with 3+ hours available + +### Prerequisites for Attendees +- Laptop with internet +- Claude Code access (or other AI coding assistant) +- Git installed +- GitHub account +- Preferred language runtime (Go, Node, Python) +- Code editor (VS Code, Cursor, etc.) + +### Reference Repository +Attendees use `github.com/emmanuelandre/unveiling-claude` as reference (NOT copy-paste). +They build their OWN project from scratch. + +### Timeline + +| Part | Duration | Content | Hands-On | +|------|----------|---------|----------| +| Part 1 | 20 min | Philosophy & setup | No | +| Part 2 | 30 min | Project setup | Exercises 1-3 | +| Break 1 | 5 min | | | +| Part 3 | 60 min | Build auth feature | Exercises 4-10 | +| Break 2 | 10 min | | | +| Part 4 | 25 min | Testing deep dive | Exercises 11-12 | +| Part 5 | 20 min | Git & best practices | Exercises 13-14 | +| Part 6 | 25 min | Final challenge | Exercise 15 | +| Wrap-up | 5 min | Summary | No | + +### Key Exercises +1. Create GitHub repository +2. Create CLAUDE.md project rules +3. Initialize Git workflow +4. Write feature specification +5. Create database schema +6. Implement repository layer +7. Create API handlers +8. Write E2E tests +9. Run and debug tests +10. Add unit tests +11. Create proper commits +12. Push and create PR +13. Final challenge (choose feature) + +--- + +## Option 3: Original Interactive Workshop (2-3 hours) + +**Use:** `claude-code-interactive-tutorial.pdf` + `prompts.md` + +(See existing guide content below) ## How to Use During Workshop @@ -115,14 +220,23 @@ Claude creates a comprehensive CLAUDE.md with architecture, commands, and conven If you need to update content: ```bash -# Update the overview presentation -python3 create_presentation.py +# Activate virtual environment (required for reportlab) +source .venv/bin/activate + +# Generate 1-hour lecture +.venv/bin/python3 create_lecture_presentation.py + +# Generate 3-hour workshop + prompts +.venv/bin/python3 create_handson_workshop.py + +# Generate original interactive workshop + prompts +.venv/bin/python3 create_interactive_presentation_v2.py -# Update the interactive workshop + prompts -python3 create_interactive_presentation_v2.py +# Generate overview presentation +.venv/bin/python3 create_presentation.py ``` -Both commands generate PDFs. The v2 script also generates `prompts.md`. +All scripts use `presentation_utils.py` for shared styling and functions. ## Using the Reference Example diff --git a/ai-first-lecture.pdf b/ai-first-lecture.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3c29d3a8a13daf8c37bdd30f42c8e33d4e31b87b GIT binary patch literal 59768 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%0D{sYkgF;sSQGGLg>y5@L+Dp!U@ z%BOYR?A|T#jXyKFyWo>s@qH_gqcbk%7f`S~ncpsIF!L6xPHTQx8b#>>73bquXUmS} zl`=KNvwe(?Q}O)+1-8np_Q&t$9}p`4yE78l1pdGNEge4`Jh}3P63Wy2*!~BNS3N+~ z8^b4U3BOn+zGu_dsQVY^wq{*=pBuLdT5bTMXRr};5R+@;N!swiMVnoYD^NL@o(}N) ze#|bVtjJTZackx=FIhy+_resKE#1d;B-f_ir(AmgZ=_e^@PPv@ZIK}7{&|2_^=S8w zFm75fG0+%79oC;+`}w;Ka^{?NCvo?y?csx ze#kKbK9G*~r65*}A$_+XPgn*of&A4#)z(pH8SDi-bM2Pa9GSp#CLyXKJ@dm^t&e}r z?7=*Y6f`3$3hDw2t}JaXil(gnn_1Xx{0%_d6ff_3HaiXTRm8=TidB(P4jpmxmu1Qi z^>k+~;oWx8#hYb2D7bHjS}us@BuT_~T4aTKgXw84fz{O1_7$NT`s^N)|u zKhU}A{NGjM*JkYFm!1M}h{Me8za~$=fSPX<>T=47UxEi5=?yMGt4}K@ZDW#59U@D{ zjkwSQNqAaV?Dn>(B5XlzrduA_Z1FME0a&l6YqeN^SA7sxzp9{NbT)0B8mwpCyI~Ww z)!3*7Apb=G;?|xm7IHB*ii_{T0cDs$U^BmgdrrHJ(C)0Fx%F+tn`tN(C)V?>}CXk0C$_&|IM zL$!g36A{>fZ`A&&@K08Rp||rA)O;KJQA&#!g#JJ|H6Fh0UV;yzZ*US1Yfv1$C+_#F z{-HhN1$7$pHcHoxUPBvG-ii3~D!Zw5PTM*n&R8WRqkGW9;gEVpW&g zq5KKjT&$Uj7f$lBOTgw|ajVv`zn#IG3V<_>?;fa$e>_9fn7t;g7&P?cM=-fJFN<3l zcnGc|x>}T`q`-PIxXT_qN_1!Ck+t0a%8dk_CDUgAzST!vFmjMQPEN}TT&-rHrZoKW zm^$}bWoH3kCu>m>TdDQjDEhh~hetBBVJCx76v*>4ylrr#UTDsJSE;MD?bBfU-p$sBgM zkz3<$+A~+wSCf~cYYU0UhfqEbuM1vS=)J7XyYaw(Ypu!s!K@VQ0R!XYzUI9yb2~aV z&#sMI%JiAb@wAwAcL~-_+cT)y{rFH)cduDpDhUDtM%HJ2RO+|07`4J z%rkNN^=QYWsIDb>BqdW^zBuQF^5_)jGc;6~$-QvctmIb9!mq&QSYa%#RCc`vz;p)i zn55TW!1@6;GGR{L!yst3Ypebu-0Q&w8K^Y`|M7NWyl>$1s%tscDu&&W zxt+{}`ml62Pl4)mK9l7_{j~@!lYdHU@*1~a#qSO9dXva>x8D=qLGxR=aJW}H2?<=f zO!DdtX9LjwB{z_B^Vct_bX;F9tIg0^j^pvEDeUpZBADR6;^S5$ffbFJH(htGpU2vI zp)leGIrb-j=u{!9RW;s%UekD)9F%V_=nNrYP;kbDYd#Y{3!qyIwJvyFqCb($6sWHA zntm5)wDKX@>i&bjN-JvXp-d0Pm$V`KZm%I%N5ckveZ|#fzUbT26jxkGpp!Hn%T2$; zJI{t=BB$rN`QUE=G2#zn4efM63ww^-q9^r}>FQxQjv#h=w`F88#3y8!^_!o#9KLis zEHUw)B9L{O`rpgv80tUP;~;QNyzeCtxMp+PYitga1jwjW3ymMe-O~ySk9xc#maFc< z7Bn)C!94b9 z6l=AO`$HAlMdg(1)f-lKrPXhI@fq##t=MQ)_C4?hEW8mWI&LzIi^@?OH~rNj-So3n zi#j5As6r_nTJGLE{N~&f&jEHq=i9dS_0iSDU|9<`Y|=VKa?fbvd9>R5XA=ugrO-IsrkJfp%p` zstNC%d2sJ!&!;t-WCzE^tS=b=P7@plg&hcYmhMgJZXI)Kfz_|)d1#dCb~HI&cgvOp z44kD-r%N2ym!feFj@BFw3tNsa#oD-JWFP2;4DB;0{8&{xe(~5(;rn~{7!2ZFNVa$S zq=F})Q#$fA2zQ9V2y~-cfCTlK?RwTBf;U3P0=GOxDU_VXWtK9v&ZVb|Lm0y68KlVT z#))F~17h6(YwF>(G&yuV*KW@(aIttcznW&aOdd29inZoyukEh0=`pCNT@V7K?79L2 z+-6Zy8_?LBhrK6Ho=DpUMA5;M>4pISGS+0Ul=8WaB=DB9&4>EraWM6Y4!>Mqqlui~|IhqW7oQ=1eN z8~+mWg_92`Ifq^}-c2{Ei0#jY&D%HjQu(^STX~4K@!LTmM-F{G=!JUIp;XA)_WJCl z1*m~1b{Wc+;Ir_7OQ;pOJf4P&n1XY-%m1b(_2jvsdEXXCzo^xka3+ZfB06iWdQMzs zFTL*fw!>N%YUG2i=E{6ft|C#Dn`LZ?C>APKUz@fe-uOd z`(@&diduCa9mJ75N(p=b!k-C1KUU@ zp!M}H5`ZxXg9d`8AXxCIb`HgguI)Ib5fy%lx$Aaj=TUj43B!dHcIHDo3ViOn(G>j^ zZsu~pF5;VM0v(-&eMRX`o}htbXHMsYdl6Q{l3enEpUAj=peh+n@_Be3Tk;HF82ujV=*^$*FmQ8!nJYso~Ite>42 zVvcl0of-t%P8Re+ZC?8hXL7|wG?`PF z)u=mgTGHA<;e?T1fu?DV!?rug=m_I>k^o;wZw{i>kt zu`R%dyfH4lc0jnPpTcwdd3%lEjnZ3Y^PWafos~Osi#u*}?oXgqct=uoaeuTZa;_?5 zLY(H0(U^RR^y;%JV5bL&W!~y);ngFOK!%fFbwb{JxJ=)cRsU+gn%uCw+Vs{=a3JjfKjLw(aJm*Ym?5S*ody<2M(X_ms>Q;3IB9pplt3mLhOvw9$=`g3p09)mD+ zi30Gw(0@Wr*6Du`MfwjQ>$yhKzyx24tek zdr+L$H&M_fkfAvtR48>Wou+H$b{s9yJV0DoFpz(j?RPyPo_;iCLvnt9n#pF6Au z&*#r@bFUAx`(nBR;QLbJYTZYZ1m0Bd39zp^j94_o9yf-6El@K{wmL>5c@;C+i z3_~aOw(z*`m*FCAlg=O!SDi(;?@UJPaNf|z@k%IO&WhZ?Ue90CJB=#rqgEEWpsd`o z_PlbUE8D)Z8~*@#ts@cq(wZf^`OaHY@YnKaP&(9T)}OpD+I4>lT|0R=EaaneED=5D zQ2S&+;;8k|_oSTDKetnAELJG#9k45$ANxP>R*T4eS$fKc%mI8c=eAy5x%6tccjapn zoyqduk8xqX5H_{k_`+;u^bsCDlGDzvs3F@$Set~_Q9sj7dNI$>)y4wM<#5+cEBMmv zB~f#I=&k$^tUrFN(9ZZx?pW-s1f6J@3qS z#6x;I$;WkSs6lO|tXr_%h+pk_Fz7rj{=D@syFkuizmWF@>1c^RVz_Lu-Tv@c(IysP z*B)(=3U%Cl#^OkLP2>mYQ?)9ad50s2HUwu(4g<#hVj*`98-^*YvEHVtdZhmLU8%A^ zZw_P;?LUYnUf&~A&fAZnKD{qVe`;o~)Biw1V*aBR?=KwD|1}-+olzwic1Pkmc`tYT zjzCb89eAP75CsS z8E*y<-1EEbVuiIgAFb0OP#7QD)XibwWppJ)GRNY) zm#3G7owL6JP_|WGFwLENf`nHHeG3KYjE;(xionc#tKp zY~8h5E3Oa~p4HCoA4lVVpn!1y-7W;)?10zuuhDqWuDMcJS}_oWAz_%4f!D)R@gre< zIoiHfgx?e7LM2gbmf$RGOB3J+R;q6UgX@5Qa;H(;f_f25iCF!jI0A6^TnV!+o$7u! z1(vD~GeytqQoXePRwv;;WlUh-JoeE(Bkr5I>#D$vxD`(Y+vyxY<7I!=yFIv~3@8{8 zn9OTYA2c|v`HB{GUo6i7>SzIQH+49uCG{6y>fA>kn#9_gs%$kE5GTh>28b!}JZzrI zo8Zz$3VdJ{yGygFIhqNC=^Ngmh6JpT9b8?_)F-?jtxJJc0@3cOGh}h52lbfi_csQ< zy5u5N=te0Y9z4%ns1B4tpw)}4z{1G&_Bt`R81~dR7cz*9wc2EUYf52qFb7^-e>AV8Bhl&g3{kFn#`F< zc%`K8jW==CQlHaO6NJ5Ip;*qVOLaNe#wVxQh|4#9>wk7#^hcz_mo@k~^}h)+*pgq9D6svV$B zl7$@HqATu3lkv*--l*9MUA~#Z2pEIGd;iN}e|%8>i5$iL*9fe5Y}YJ%)~`q^GEtn& zCS&c+43+FaDGA-35`7RAqq?BSPI5LH>wiZV;0qO+nnm`T->GlwqRIZwCkd-NH@ zMD=9uzS9eST9u>8___S)PCvOMBR);z;a@XRMD2k(sE+z2GMXYGh# z7y8gC_p@7rL7$=daNp6KVSR!tk?pfNt5wgwWtkgza8)ne2N-MffyaZ&L7+>w?>Gsy zW$XrN^XWDL)7kgnPp~Y(I_>ky_`rd2fm-y~tK_DG&jD?ME6hpkqp6?zySpx)p+zi^ zQZv#wt0gN9_9dS{(gr6T*6OOVkNbmcQ9V|0+|kZB$m;>_$sEJq{Z+Hky2ZzeGb0Xw z{v3{PSt$K>+Nx`JXLO~H9Fov56Z=sn)3kEqP%(fC{T^ow5>bNqMuEL z{f{^}10hObr~xnFRH{UovRfYF^Y~B$RNg)q93Pd_xBmDZ*k}_M>EXK>PwivauD?dY zhr1xzsgwT>#`YNc?Y}z8o0oEtM%Df<+`#iiv@NbfDZPap4SYdtS5G#6+_~i~CHKGr zWe~R1%F0vMP)}a;dCr<6^XZ;7_tK~c105;>M$2Tu@ZpcM%acL|o#Zn5UoYGZX*B zk&c*%Iepg^Vpb+uS!HGB_r<9RAm)e@^k_Y8^M0|W$%Bh;nwpp1mx-Y^Y3ovJl+z+- zW=&=G6W8bZH-xO6Gglw?F#FK4R`>FcYv?a>QXKc^#n_ur+|H~8n0MX$AT6b=lh5`_oP<2I7D21j&1+fT?Hl#!GlXaq3Pvs7*r zT?EJ?`G|<6z@7yMzEZ*3Hyj{a%RZm#l>i0lEZ?@`m|!-dvL<$2|K30H`KnxeY)`pM z$0_-du`Bn5x1PAtvjv*t&wTo}378cfkDduEl{r#+IT+gSN^Y21yNu;#TWUYO9h<9x zH)wlzy_6WRh0$F-oEJBMwX%h0hu`rslrtMy!-zdlzF<*j?0!BuL?v;QJDsEu>D(W~ zSOLkkkoL{aZ7W_mkoz$@F7KgE0J08>D+AFSotxAzRb@~$&fZE%s92G_wz{}z9Y*nz zk2(aV>7;BS{8h|am~3E*iK&OU22DD!m(0pQ-L$4EIKFH!9h1saeNXjH{W`o{ zS9#+`@po}ITH}R48(`UXqFNi)&$ZZMFeK)d7@tgb-3!~T3kHw2lHd0w&uFRq?043$ zEplsPBXc81=Id*5p#|5VK2z^oNI@V=*2Gt@yCXNYNFBL00XTgvS+i`c6%5cywcc3P z_i@dO0+*oKy4S|a9MwAuhvPfMxJ(7iAm z-%3ntRZg9o8+@)KB%!|JNMn{Br;=mgMng}*vNCDG-Q+#Fx!jVdmAM8pOkEj7W=<>~ zZFA#$l#q@Nt8>?Mc0r9)XjNw$w#rGhz5Z zbpH8Vh#FQu4AQEsXw^13E!dFkD@kW%#Nit8rUAfYEl$*$=C|V~?wzkcs*avQHA&%W zr^focVxXtJkpR5nY8I}od?xS8#_PeamvxrB)?VnYO3fs>_?lreQO2%g9WL);M&oEp zYJB*rK1%0DPSET`0nA2W;8Ut|^mS?mw|u18VXl{0t!e_GVlou7OX z3H&DP`8&X}$wk2K$hn*M$s z{pRtSc8k;0PD$D7md>6suR=4Hj^5I})Jx`1Y_v8dqL-Y>^j~>J^^fwYbe^jrlWhqm)J$sO6jdj3*EIDKgBJ&VrLG-i(?`;F36SI!L%a$6*NtFkdiBK+yBTFGlVzAVead*8k$Pb5G(q6I_&cJ-rPSNPoY?)6 zA>n*H9mEnbfGF~?!*aILpd@nLnH8T6byZ1Z&6x5g1_sawF1*<-adflS`vXnl%BiI_ z&~cBt=J34Mj_tN1$9Inl@2;T3u6wQ}>+#62N{P@E-&|Q@k6cGxhX7KBo|@`PtD)ht zNg$$h>x;$Agj+A4ZICsLv^v8KYeB4Bt47~2Cwr*8>?7O!T+3b1xSk@u`y{yKuIGaAe*w$OQVywO-*uWQ*7+mG-E`J3xb;)Gp9|l;iTV9_|-_Yll1`%o3%+OP)J{& zh3TkLa$8S4VtLdKIGA5?eWu1LXW&)KqSNzH9knMV8CnlU1r^83uA7Yc`+X3(C$vky zLUYDa8Y3KHuC{s9>7R_-m1u3F(7C1>ia{<_$n=}@ReN`8ms0mN4|E!^(o5a0NdQi- zEuAM=qV*G+G8BpZ|8uVVr>~Pa3E$kOzq$UB+c_P`k86pKroCQi+*OM+U>>K+S9(8F z#__FOnA!G~5u#2d$N<{XKwn?R_aK0RjXCgKb(KSRsh#4v$S!+lnh%PK)#}!&y1pIz zw}I7rweDJGJZAe-=?vXu$DKoWjN4}Cp>^lJm6SY<*3+Gn7>_*d0;gsYhezV=9zbIl|Ng%5V5)OBLt zKq#~7?78dVEzRz_OsUKx2vV`Ig&hlW)qUzbkSgvDa?3G`#!+EpLFEwk@(=~`9@&|Y0s!Keyt!a9Bnz;<3HQtib_-oIfk*_&%R*2lvf0j@@;ySu9FCdi7Vfl> z<45y%Kb^=dH{;DVl1mVSThSQc4PA|G%KhR_#kT11o7a|96zW(el1#Og0!+s1tctjr zc03K6#i%RUoV=|;KSeApazKmZ#PjVa5Q_A-Q0(DfiH0|GW#tacmZT+HON&a!^llI< z>0P+}EgrfRSBeb=cBZg9Z`Pi*V&a*XWgjkzwtA9=Os)<)lzjK|v(238 ztWQC|cw25_DQZJTmallH>bR@n(CC^s2FYfcRrj!WIKHr5zpP%-IpjF0*W48arGvEM z+18ogQ0Mw3BHW|hc5IzZPZayqXjA3t4M`foT6x_b<4u0$uM5wDVV#ad#`W5JV}KC~IkpSDXe|KhA|)J zV$TnmhLT;My6$j%K>K>3dCyA4CprN5lSsMk@5=Km6l2P^=aVYuFN;#;oeR8VUw|kf zE%uY<3^f*B{@Ej;DLG%OCXi#JuDLTSKQa?iprtbfp~@f3$1I^=Zv#?8&+(Ns_Sv4& z?W`A5uVxLCrS^D#@~BxbUeyPRUK$7hFZ|;jHvJcw9{K|c;W1(V8*~o1roGN^i)jj5 zx=Ac1A_no)Pmuuo=T)DcUwJ>jsE$1B%g?2i30sh0>RGa7Bxj9DxmmOGw zy46EEBVGo%cT!8xHDQ$z$+4%Dc$@KbXrIS2(d=}~?C}^05a17hK02!^-pO8EWEJcI zWW?gy?}nL+w8uVwiUX<(9D+M-35zF-t^Ofc$RwwYa%L&8l1oB!00ddlIX=G3#YG~* z;Z+R~f^AtoSjW3d*7uBvd&&!FSh$pyOGG{3Z7};s&RxhR(SGy5WrZWXF+giOk_iPh$I`*L!{Zz zyM#wWdWd{@or@y-sRy7{GoN~p1kHTvK@u$UsRv2$%%>hCAu^wOkYqRxL31nfsRs$_ ze(FJzP_y=7D-!nPkRB9ChxDLmI-~~$4*mIk6gWp{3=I7_25Ub&1{eUL&xO}C$^4)% z3s(JnmZd}Ml4Qf{m*lum`~y>k;t(kSVBx0>;I9eE6&c~vrWE+Wr(GyWd4GN%MUnjH zdjlGQ&^o7RA+%O0h6=?x;H?koL9rqcnhVE;)&d1C`T0I5UW8ro&$t0r3E7?!*-(6; z2|A=34L10+A2iT~e2Au)kU!IOIR3#_@y+i3M_i*BJ`_s;bw0GFA*zLJKy!TPokJ84 z&rg6A_bFEpL-7xGve5T2M0mesfcoJxUct5%vK2#7)aScoXd-mBfbA_Tm*GNwz_0}Q zxz1r548=8u1J3dfeHl&=KYf>hJs_lqK!$vm5kxU0R}?}vU=&T3rnB)e3m7J z(AkHj!amN@4EZ@fmLWs2on_e2J_Cow@ciI-^=Si^1MC0qoL>*?bafuT3WZ;Iv-n&p0BUe@%6grar~cu{=pu7fkAQ@V|Jl@|XDw qH+}#0^&JMj{~4VA)A@aNE*5`1U)JOG`#8;V0$h|N61A@O>%Rbp=WTlc literal 0 HcmV?d00001 diff --git a/ai-first-workshop.pdf b/ai-first-workshop.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0cc007ea11a4e4f214431bfb950addee63b9cc75 GIT binary patch literal 64550 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