diff --git a/Agents.md b/Agents.md index b68be1d2..792781ac 100644 --- a/Agents.md +++ b/Agents.md @@ -1,5 +1,9 @@ # Markets — Project Coding Standards +> This file extends the **[petry-projects organization AGENTS.md](https://github.com/petry-projects/.github/blob/main/AGENTS.md)** with Markets-specific conventions. The org standards define TDD, SOLID, CLEAN, DRY, DDD, KISS, YAGNI, pre-commit checks, CI gates, BMAD workflows, and multi-agent isolation. This file does not restate those — it specifies how they apply to this project's Go + React Native/Expo stack. If a rule here conflicts with the org AGENTS.md, this file takes precedence. +> +> For detailed Markets-specific application of each principle (bounded contexts, aggregate roots, repository interfaces, domain events, typed IDs, dependency direction, test-by-layer guidance, coverage thresholds, and CI commands), see `_bmad-output/planning-artifacts/coding-standards.md`. + ## Project Overview **markets** is a real-time coordination platform for local farmers markets. Mobile-first (iOS/Android) with web via React Native for Web. Three user roles: Customer, Vendor, Market Manager. @@ -148,14 +152,12 @@ theme: { ## Test-Driven Development -- IMPORTANT: All development MUST follow test-driven development (TDD) practices -- Write failing tests BEFORE writing implementation code (Red → Green → Refactor) -- **Go backend:** Write Go test files (`_test.go`) co-located with source files before implementing resolvers, middleware, or services. Use `go test ./...` to verify. -- **React Native frontend:** Write tests using Jest + React Native Testing Library before implementing components and hooks. Test files co-located as `*.test.tsx` or in `__tests__/` directories. -- **GraphQL resolvers:** Write integration tests (tagged `//go:build integration`) that test the full resolver → database → response path -- **Acceptance criteria drive tests:** Each story's Given/When/Then acceptance criteria should map directly to test cases -- Every PR must include tests that cover the new or changed functionality -- Do not merge code without passing tests +TDD rules are defined in the [org AGENTS.md](https://github.com/petry-projects/.github/blob/main/AGENTS.md). Markets-specific test framework configuration, mocking strategy, coverage thresholds, and per-layer test guidance are in `_bmad-output/planning-artifacts/coding-standards.md`. + +- **Go backend:** `*_test.go` co-located with source; `//go:build integration` for integration tests +- **React Native frontend:** `*.test.tsx` co-located or `__tests__/`; Jest + React Native Testing Library +- **GraphQL resolvers:** Integration tests for the full resolver → database → response path +- **Acceptance criteria drive tests:** Each story's Given/When/Then maps directly to test cases ## Event-Driven Architecture diff --git a/CLAUDE.md b/CLAUDE.md index 2465c655..cb0a6baa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Markets — Claude Code Instructions -> **Read [Agents.md](Agents.md) first.** It contains the project coding standards, technology stack, component organization, styling rules, naming conventions, accessibility requirements, mobile UX constraints, TDD practices, event-driven architecture rules, and anti-patterns that apply to all work in this repo. +> **Read the [petry-projects org AGENTS.md](https://github.com/petry-projects/.github/blob/main/AGENTS.md) and then [Agents.md](Agents.md).** The org file defines cross-cutting standards (TDD, SOLID, CLEAN, DRY, DDD, KISS, YAGNI, CI gates, BMAD). The project Agents.md contains Markets-specific technology stack, component organization, styling rules, naming conventions, accessibility requirements, mobile UX constraints, event-driven architecture rules, and anti-patterns. For detailed principle application (bounded contexts, aggregates, typed IDs, test strategy, coverage thresholds), see `_bmad-output/planning-artifacts/coding-standards.md`. ## Figma MCP Integration Rules diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml new file mode 100644 index 00000000..19d04015 --- /dev/null +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -0,0 +1,116 @@ +# generated: 2026-03-28 +# last_updated: 2026-03-28 +# project: markets +# project_key: NOKEY +# tracking_system: file-system +# story_location: {project-root}/_bmad-output/implementation-artifacts + +# STATUS DEFINITIONS: +# ================== +# Epic Status: +# - backlog: Epic not yet started +# - in-progress: Epic actively being worked on +# - done: All stories in epic completed +# +# Epic Status Transitions: +# - backlog → in-progress: Automatically when first story is created (via create-story) +# - in-progress → done: Manually when all stories reach 'done' status +# +# Story Status: +# - backlog: Story only exists in epic file +# - ready-for-dev: Story file created in stories folder +# - in-progress: Developer actively working on implementation +# - review: Ready for code review (via Dev's code-review workflow) +# - done: Story completed +# +# Retrospective Status: +# - optional: Can be completed but not required +# - done: Retrospective has been completed +# +# WORKFLOW NOTES: +# =============== +# - Epic transitions to 'in-progress' automatically when first story is created +# - Stories can be worked in parallel if team capacity allows +# - SM typically creates next story after previous one is 'done' to incorporate learnings +# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) + +generated: 2026-03-28 +last_updated: 2026-03-28 +project: markets +project_key: NOKEY +tracking_system: file-system +story_location: "{project-root}/_bmad-output/implementation-artifacts" + +development_status: + # ── Epic 1: Authentication & Role-Based Access ── + epic-1: backlog + 1-1a-frontend-project-scaffolding: backlog + 1-1b-backend-project-scaffolding-audit-infrastructure: backlog + 1-2-google-apple-sign-in-authentication: backlog + 1-3-role-selection-user-record-creation: backlog + 1-4-market-scoped-manager-permissions: backlog + 1-5-role-based-access-enforcement-middleware: backlog + epic-1-retrospective: optional + + # ── Epic 2: Market Administration ── + epic-2: backlog + 2-1-create-edit-market-profile: backlog + 2-2-market-schedule-hours-management: backlog + 2-2a-browse-invite-vendors-to-market: backlog + 2-2b-future-market-day-planning: backlog + 2-2c-market-rules-expectations: backlog + 2-2d-send-vendor-notifications: backlog + 2-2e-cancel-or-end-market-early: backlog + 2-3-per-date-vendor-roster-management-join-request-approval: backlog + epic-2-retrospective: optional + + # ── Epic 3: Vendor Profile & Product Catalog ── + epic-3: backlog + 3-1-create-edit-vendor-profile: backlog + 3-2-product-catalog-management: backlog + 3-3a-browse-search-markets-to-join: backlog + 3-3-market-association-calendar-date-selection: backlog + epic-3-retrospective: optional + + # ── Epic 4: Market-Day Vendor Operations ── + epic-4: backlog + 4-1-vendor-check-in-conflict-detection: backlog + 4-2-exception-status-updates: backlog + 4-3-product-availability-updates: backlog + epic-4-retrospective: optional + + # ── Epic 5: Manager Dashboard & Market-Day Oversight ── + epic-5: backlog + 5-1-live-attendance-dashboard: backlog + 5-2-request-vendor-confirmation: backlog + 5-3-manager-check-in-on-behalf-of-vendor: backlog + 5-4-market-level-operational-updates: backlog + 5-5-automatic-vendor-checkout-at-market-close: backlog + epic-5-retrospective: optional + + # ── Epic 6: Customer Discovery, Search & Follow ── + epic-6: backlog + 6-0-customer-onboarding-location-preferences-first-follows: backlog + 6-1-market-vendor-search-distance-filtering: backlog + 6-2-vendor-profile-market-status-view: backlog + 6-3-follow-vendors-markets: backlog + 6-4-following-feed-live-status: backlog + 6-5-alternative-vendor-discovery: backlog + epic-6-retrospective: optional + + # ── Epic 7: Notifications, Activity Feeds & Real-Time ── + epic-7: backlog + 7-1-push-notification-infrastructure: backlog + 7-2-vendor-check-in-checkout-notifications: backlog + 7-3-exception-market-update-notifications: backlog + 7-4-notification-preferences: backlog + 7-5-per-role-activity-feeds: backlog + epic-7-retrospective: optional + + # ── Epic 8: Audit, Privacy & Account Governance ── + epic-8: backlog + 8-1-immutable-audit-log-infrastructure: backlog + 8-2-activity-log-all-roles: backlog + 8-3-account-deletion-soft-delete: backlog + 8-4-governance-rules-enforcement: backlog + epic-8-retrospective: optional diff --git a/_bmad-output/planning-artifacts/coding-standards.md b/_bmad-output/planning-artifacts/coding-standards.md new file mode 100644 index 00000000..9a3c8829 --- /dev/null +++ b/_bmad-output/planning-artifacts/coding-standards.md @@ -0,0 +1,697 @@ +# Markets — Project Coding Standards + +> Authored by Winston (Architect) during Epic 1 Sprint Planning. +> This document defines how the Markets project applies the organization-wide engineering standards to its Go + React Native/Expo stack. + +--- + +## Organization Standards (Required Reading) + +All development in this repository follows the **[petry-projects organization AGENTS.md](https://github.com/petry-projects/.github/blob/main/AGENTS.md)**. That document is the authoritative source for: + +- **TDD** — Red-Green-Refactor workflow, coverage enforcement, no `.skip()`, no coverage-ignore comments +- **SOLID** — SRP, OCP, LSP, ISP, DIP definitions and general guidance +- **CLEAN Code** — Meaningful names, small functions, minimal arguments, no side-effect surprises, error handling +- **DRY** — Knowledge deduplication, premature abstraction warnings +- **DDD** — Ubiquitous language, bounded contexts, aggregate roots, value objects, repository pattern +- **KISS / YAGNI** — Simplest solution, no speculative features +- **Defensive coding** — Validate at boundaries, trust internal code, fail fast +- **Separation of concerns** — Layered architecture, inward dependencies, no framework bleed +- **Code organization** — Co-located tests, no barrel files, consistent naming +- **Pre-commit checks** — Format, lint, type check, tests before every commit +- **CI quality gates** — CodeQL, SonarCloud, CodeRabbit, Copilot review, tests, coverage +- **BMAD Method** — Spec-driven development, planning artifacts, story-based implementation +- **Multi-agent isolation** — Git worktrees, one agent per story, no overlapping file ownership +- **Security** — No committed secrets, environment variables for credentials + +**This document does not restate those principles.** It specifies only how they are applied in the Markets codebase. + +If a rule here conflicts with the org AGENTS.md, **this document takes precedence** (per org policy). + +--- + +## 1. TDD — Markets Application + +### Testing Framework & Configuration + +| Layer | Framework | Location | Environment | +|-------|-----------|----------|-------------| +| Go unit tests | `go test` | `*_test.go` co-located with source | Go test runner | +| Go integration tests | `go test -tags=integration` | `*_test.go` with `//go:build integration` | Real Cloud SQL (test instance) | +| React Native unit/component | Jest + React Native Testing Library | `*.test.tsx` co-located or `__tests__/` | jsdom via Jest | +| GraphQL resolver integration | `go test -tags=integration` | `internal/graph/*_test.go` | Full resolver → DB → response | +| E2E | Maestro or Detox | `e2e/` directory | Real app on simulator | + +### What to Test (by Layer) + +| Layer | What to Test | How | +|-------|-------------|-----| +| **Go resolvers** (`internal/graph/`) | Input validation, authorization, business logic, error codes | Unit test with mocked DB; integration test with real DB | +| **Go middleware** (`internal/auth/`) | JWT validation, role extraction, context population, rejection of invalid tokens | Unit test with crafted JWTs | +| **Go event bus** (`internal/events/`) | Event publishing, handler registration, handler dispatch, failure isolation | Unit test with mock handlers | +| **Go DB queries** (`internal/db/`) | CRUD operations, soft-delete filtering, constraint enforcement | Integration test with test database | +| **PostgreSQL migrations** (`migrations/`) | Schema creation, audit trigger attachment, rollback safety | Integration test: migrate up, verify schema, migrate down | +| **React Native components** (`components/`) | User interactions, conditional rendering, accessibility labels | React Native Testing Library with user-event | +| **React Native hooks** (`hooks/`) | State transitions, Apollo cache interactions, return values | `renderHook()` from React Testing Library | +| **Apollo Client** (`lib/apollo.ts`) | Cache policies, optimistic responses, error handling | Mock Apollo Provider in tests | +| **GraphQL codegen output** (`graphql/generated/`) | Type correctness (compile-time only) | TypeScript compiler (`tsc --noEmit`) | + +### Mocking Strategy + +- **Go backend:** Use interfaces for all dependencies (DB, Firebase, event bus). Create `mock_*` or `fake_*` implementations for unit tests. Use real Cloud SQL test instance for integration tests. +- **React Native:** Mock Apollo Client with `MockedProvider`. Mock `expo-secure-store`, `expo-notifications`, and Firebase SDK. Never import Go backend code in frontend tests. + +### Coverage Thresholds + +| Metric | Minimum | Target | +|--------|---------|--------| +| Go line coverage | 80% | 90% | +| Go branch coverage | 75% | 85% | +| React Native line coverage | 80% | 90% | +| React Native branch coverage | 75% | 85% | + +--- + +## 2. SOLID — Markets Application + +### Single Responsibility + +**Go backend:** +- One resolver file per GraphQL schema domain (`auth.resolvers.go`, `market.resolvers.go`, etc.) +- Business logic in domain packages, not in resolvers directly +- Event handlers are separate from event publishers + +``` +GOOD: internal/auth/middleware.go (JWT validation), internal/auth/claims.go (claim extraction) +BAD: internal/auth/auth.go (validates JWT + queries DB + publishes events + sets headers) +``` + +**React Native:** +- One component per file (except small internal helpers) +- Hooks encapsulate one concern (`useVendorStatus`, `useMarketSearch`) +- Domain components in `components/{domain}/`, not mixed + +### Open/Closed — Extension Points + +- Go event bus: add new handlers without modifying the publisher or existing handlers +- GraphQL schema: new types/fields don't break existing queries +- React Native: new Gluestack component variants via `tva()` without modifying base components +- Audit triggers: attach to new tables without modifying the trigger function + +### Interface Segregation + +**Go:** +```go +// GOOD: Focused interfaces +type SessionStore interface { + GetSession(ctx context.Context, id string) (*Session, error) + SaveSession(ctx context.Context, s *Session) error +} + +type AuditQuerier interface { + QueryAuditLog(ctx context.Context, filter AuditFilter) ([]AuditEntry, error) +} + +// BAD: God interface +type Database interface { + GetSession(...) (*Session, error) + SaveSession(...) error + QueryAuditLog(...) ([]AuditEntry, error) + GetMarket(...) (*Market, error) + // ... 30 more methods +} +``` + +**GraphQL schema:** Split by domain (6 schema files), not one monolithic schema. + +### Dependency Inversion + +**Go:** Constructor injection in `cmd/api/main.go` (composition root). No DI frameworks. + +```go +// cmd/api/main.go — composition root +db := connectDB(cfg.DatabaseURL) +authMiddleware := auth.NewMiddleware(firebaseApp) +eventBus := events.NewBus() +realtimeHandler := realtime.NewHandler(firebaseDB) +notifyHandler := notify.NewHandler(fcmClient) +eventBus.Subscribe(realtimeHandler) +eventBus.Subscribe(notifyHandler) +resolver := graph.NewResolver(db, eventBus) +``` + +**React Native:** React Context for dependency injection. Apollo Provider, Auth Context, and Theme Provider wrap the app at the root. + +--- + +## 3. DRY — Markets Application + +### Go Backend + +- **Reusable query helpers** in `internal/db/` for common patterns (soft-delete filtering, pagination, market-scope checks) +- **Single audit trigger function** attached to all domain tables — never duplicate trigger logic +- **Shared error formatting** — one function to create GraphQL error responses with proper codes +- **Auth context extraction** — one helper to get user ID + role from context, used by all resolvers + +### React Native + +- **Gluestack UI components** from `components/ui/` are the single source for base UI elements — never recreate buttons, inputs, text +- **Domain components** in `components/{domain}/` are composed from base UI — reuse across screens +- **Custom hooks** encapsulate reusable logic (e.g., `useOptimisticMutation` wraps Apollo mutation + MMKV queue) +- **Design tokens** in `config.ts` — never hardcode colors, spacing, or typography values +- **GraphQL codegen** — never manually type API response shapes + +--- + +## 4. CLEAN Code — Markets Application + +### Error Handling + +**Go:** +```go +// GOOD: Handle every error explicitly +user, err := r.db.GetUser(ctx, userID) +if err != nil { + return nil, gqlError(ctx, "INTERNAL", "failed to fetch user") +} +if user == nil { + return nil, gqlError(ctx, "NOT_FOUND", "user not found") +} + +// BAD: Swallowing errors +user, _ := r.db.GetUser(ctx, userID) +``` + +**React Native:** +- Apollo `onError` for network/GraphQL errors — show user-friendly messages +- Never show raw error messages, stack traces, or error codes to users +- Error messages are short and action-oriented: "Check-in failed. Tap to retry." + +### No Side Effects in Unexpected Places + +- Go resolvers: write to DB, then publish events. Never publish events without a successful write. +- React components: side effects only in `useEffect` or event handlers, never during render +- GraphQL queries: never mutate state; mutations: always return updated state + +--- + +## 5. DDD — Markets Application + +### Bounded Contexts + +Each `internal/` package maps to one bounded context with clear responsibilities and ownership. + +| Context | Package | Responsibility | Aggregate Root | +|---------|---------|---------------|----------------| +| **Auth** | `internal/auth/` | JWT validation, role extraction, session variable injection | — (stateless middleware) | +| **Graph** | `internal/graph/` | GraphQL resolver orchestration, input validation, response shaping | — (delegates to domain) | +| **Market** | `internal/market/` | Market lifecycle, schedule, roster, 2-manager invariant | `Market` | +| **Vendor** | `internal/vendor/` | Vendor profile, product catalog, check-in lifecycle, exception status | `Vendor`, `CheckIn` | +| **Customer** | `internal/customer/` | Customer profile, follows, preferences, discovery | `Customer` | +| **Notification** | `internal/notify/` | FCM dispatch, notification preferences, device token management | — (infrastructure) | +| **Realtime** | `internal/realtime/` | Firebase Realtime Database writes for status propagation | — (infrastructure) | +| **Events** | `internal/events/` | Domain event bus: publish/subscribe, handler registration | — (infrastructure) | +| **Audit** | `internal/audit/` | Audit query helpers (writes are PostgreSQL triggers — not application code) | `AuditEntry` (read-only) | +| **DB** | `internal/db/` | Connection pool, migration runner, shared query utilities | — (infrastructure) | + +### Aggregate Roots + +Aggregates enforce business invariants. External code accesses children only through the root. + +```go +// internal/market/market.go +type Market struct { + ID MarketID + Name string + Location Location + Schedule Schedule + Managers []ManagerAssignment // invariant: len >= 2 + deletedAt *time.Time +} + +// Enforces 2-manager minimum (FR41a) +func (m *Market) RemoveManager(managerID UserID) error { + if len(m.Managers) <= 2 { + return ErrMinimumManagersRequired + } + // ... remove logic + return nil +} + +// internal/vendor/checkin.go +type CheckIn struct { + ID CheckInID + VendorID VendorID + MarketID MarketID + Status CheckInStatus // checked-in, checked-out + CheckedAt time.Time + CheckedBy Attribution // self or manager-on-behalf +} + +// Enforces conflict detection (FR14) +func NewCheckIn(vendorID VendorID, marketID MarketID, existingCheckIns []CheckIn) (*CheckIn, error) { + for _, ci := range existingCheckIns { + if ci.Status == StatusCheckedIn && ci.MarketID != marketID { + return nil, ErrAlreadyCheckedInElsewhere{MarketID: ci.MarketID} + } + } + // ... create logic +} +``` + +**Rules:** +- Aggregates validate their own invariants — resolvers do not contain business rules +- Aggregates are created via factory functions (`NewMarket`, `NewCheckIn`) that enforce invariants from birth +- Aggregates return domain errors (not HTTP/GraphQL errors) — resolvers translate to GraphQL error codes + +### Value Objects (Typed IDs) + +Distinct types for IDs prevent accidental swapping at compile time. + +```go +// internal/domain/ids.go +type UserID string +type MarketID string +type VendorID string +type CheckInID string +type AuditEntryID string + +// Constructor functions validate format +func NewUserID(raw string) (UserID, error) { + if raw == "" { + return "", ErrEmptyID + } + return UserID(raw), nil +} +``` + +```go +// Compiler catches misuse: +func GetVendor(id VendorID) (*Vendor, error) { ... } + +marketID := MarketID("abc") +GetVendor(marketID) // ← compile error: cannot use MarketID as VendorID +``` + +**Other value objects:** +- `Location` — lat/lng pair, validated range +- `Schedule` — operating days + hours, validated consistency +- `Attribution` — actor ID + role + source (self vs manager-on-behalf) + +### Repository Interfaces (Ports) + +Domain code defines repository interfaces. Infrastructure implements them. Domain code never imports `pgx` or any database driver. + +```go +// internal/market/repository.go (port — lives with domain code) +type MarketRepository interface { + FindByID(ctx context.Context, id MarketID) (*Market, error) + Save(ctx context.Context, m *Market) error + FindByManager(ctx context.Context, managerID UserID) ([]*Market, error) + SearchByLocation(ctx context.Context, lat, lng float64, radiusKm float64) ([]*Market, error) +} + +// internal/vendor/repository.go +type VendorRepository interface { + FindByID(ctx context.Context, id VendorID) (*Vendor, error) + Save(ctx context.Context, v *Vendor) error + FindActiveCheckIns(ctx context.Context, vendorID VendorID) ([]CheckIn, error) +} + +// internal/db/market_repo.go (adapter — lives in infrastructure layer) +type PgMarketRepository struct { + pool *pgxpool.Pool +} + +func (r *PgMarketRepository) FindByID(ctx context.Context, id MarketID) (*Market, error) { + // pgx query with WHERE deleted_at IS NULL +} +``` + +**Rules:** +- Port interfaces live alongside the domain code they serve (`internal/market/repository.go`) +- Adapter implementations live in `internal/db/` (infrastructure layer) +- Unit tests use fake/mock implementations; integration tests use real `Pg*Repository` +- Repositories always filter `WHERE deleted_at IS NULL` — callers never need to remember this + +### Domain Events + +Events decouple bounded contexts. Cross-domain communication is via events, not direct imports. + +```go +// internal/events/types.go +type Event interface { + EventType() string +} + +type VendorCheckedIn struct { + VendorID VendorID + MarketID MarketID + Timestamp time.Time +} +func (e VendorCheckedIn) EventType() string { return "vendor.checked_in" } + +type UserCreated struct { + UserID UserID + Role string +} +func (e UserCreated) EventType() string { return "user.created" } + +type ManagerAssigned struct { + ManagerID UserID + MarketID MarketID +} +func (e ManagerAssigned) EventType() string { return "manager.assigned" } +``` + +```go +// internal/events/bus.go +type Handler interface { + Handle(ctx context.Context, event Event) error +} + +type Bus struct { + handlers []Handler +} + +func (b *Bus) Publish(ctx context.Context, event Event) { + for _, h := range b.handlers { + if err := h.Handle(ctx, event); err != nil { + slog.Error("event handler failed", + "event", event.EventType(), + "error", err, + ) + // Handler failure logged but does NOT roll back the DB write + } + } +} +``` + +**Rules:** +- Events are published AFTER successful database writes — never before, never speculatively +- Events are NOT published if the DB write fails +- Each handler is independently testable and can fail without blocking other handlers +- Audit logging is NOT a domain event handler — it is guaranteed at the database level via PostgreSQL triggers +- Handler failure is logged but never rolls back the originating write (NFR7) + +### Dependency Direction (Import Rules) + +``` + ┌─────────────────────────┐ + │ cmd/api/main.go │ ← Composition root (wires everything) + └────────────┬────────────┘ + │ imports + ┌────────────▼────────────┐ + │ internal/graph/ │ ← Resolver layer (orchestration) + └────────────┬────────────┘ + │ imports + ┌──────────────────────┼──────────────────────┐ + │ │ │ +┌─────────▼────────┐ ┌─────────▼────────┐ ┌─────────▼────────┐ +│ internal/market/ │ │ internal/vendor/ │ │ internal/customer/│ ← Domain layer +│ (aggregates + │ │ (aggregates + │ │ (aggregates + │ +│ repository port) │ │ repository port)│ │ repository port) │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + ▲ ▲ ▲ + │ implements │ implements │ implements +┌─────────┴────────┐ ┌─────────┴────────┐ ┌─────────┴────────┐ +│ internal/db/ │ │ internal/notify/ │ │ internal/realtime/│ ← Infrastructure +│ (Pg*Repository) │ │ (FCM handler) │ │ (Firebase handler)│ +└──────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +**Hard rules:** +- Domain packages (`market/`, `vendor/`, `customer/`) import NOTHING from infrastructure (`db/`, `notify/`, `realtime/`) +- Domain packages never import each other — cross-domain communication is via domain events +- `internal/graph/` (resolvers) may import domain packages but not infrastructure directly +- Only `cmd/api/main.go` knows about all packages — it is the only place where concrete types are wired to interfaces +- Only `internal/db/` imports `pgx` +- Only `internal/auth/` imports `firebase-admin-go` for JWT validation +- Only `internal/notify/` imports `firebase-admin-go` for FCM dispatch +- Only `internal/realtime/` imports Firebase Realtime Database client + +### Anti-Corruption Layer + +External SDK types (Firebase, pgx, gqlgen models) must not leak into domain code. Map external types to domain types at the boundary. + +```go +// internal/auth/middleware.go — maps Firebase JWT claims → domain context +func (m *Middleware) extractUser(token *auth.Token) (UserID, string, error) { + uid, err := NewUserID(token.UID) // Firebase string → domain UserID + role := token.Claims["role"].(string) // Firebase claim → plain string + return uid, role, err +} + +// internal/db/market_repo.go — maps pgx rows → domain Market +func (r *PgMarketRepository) FindByID(ctx context.Context, id MarketID) (*Market, error) { + row := r.pool.QueryRow(ctx, "SELECT ...", string(id)) + // Map SQL columns → domain Market struct + // This is the ONLY place that knows about the DB schema for markets +} +``` + +**Rules:** +- gqlgen-generated model types (`internal/graph/model/`) are API types, not domain types — map between them in resolvers +- Domain types own validation and invariants; API types are DTOs +- If the mapping is trivial (same fields), still keep the types separate — they change for different reasons + +--- + +## 6. Pre-Commit Quality Checks — Markets Configuration + +### Go Backend + +```bash +# .githooks/pre-commit (Go) +golangci-lint run ./... +go test ./... -short -count=1 +``` + +### React Native Frontend + +```bash +# .husky/pre-commit (TypeScript) +npx lint-staged && npx tsc --noEmit +``` + +**lint-staged configuration** (in `package.json`): + +```json +{ + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --fix --max-warnings 0", + "prettier --write" + ], + "*.{css,json,md}": [ + "prettier --write" + ] + } +} +``` + +### Per-Commit Checks + +| Check | Tool | Target | Purpose | +|-------|------|--------|---------| +| Go lint | golangci-lint | Backend | Catch bugs, style issues, performance problems | +| Go tests (short) | go test -short | Backend | Fast unit tests only | +| TS lint + autofix | ESLint | Frontend | Catch floating promises, unused code | +| TS format | Prettier | Frontend | Consistent formatting | +| TS type check | tsc --noEmit | Frontend | Catch type errors | + +--- + +## 7. CI Quality Gates — Markets Configuration + +### Backend CI + +| Gate | Command | Blocks PR? | Purpose | +|------|---------|-----------|---------| +| Lint | `golangci-lint run ./...` | Yes | Code quality | +| Unit tests | `go test ./... -short` | Yes | Regressions | +| Integration tests | `go test -tags=integration ./...` | Yes | Full-path validation | +| Coverage | `go test -coverprofile=coverage.out` | Yes (80% line) | Adequate test coverage | +| Build | `go build ./cmd/api/` | Yes | Compilation | +| Schema validation | `go run github.com/99designs/gqlgen validate` | Yes | GraphQL schema consistency | + +### Frontend CI + +| Gate | Command | Blocks PR? | Purpose | +|------|---------|-----------|---------| +| Type check | `tsc --noEmit` | Yes | Type errors are bugs | +| Lint | `eslint . --max-warnings 0` | Yes | Code quality | +| Format | `prettier --check .` | Yes | Formatting consistency | +| Unit + component tests | `jest --ci` | Yes | Regressions | +| Coverage | `jest --coverage` | Yes (80% branch/line) | Adequate test coverage | +| Codegen | `graphql-codegen --check` | Yes | Types match schema | + +These are in addition to the org-level gates (CodeQL, SonarCloud, CodeRabbit, Copilot review). + +--- + +## 8. Go-Specific Standards + +### Error Types + +Use structured GraphQL error codes consistently: + +| Code | HTTP Analog | When | +|------|------------|------| +| `UNAUTHENTICATED` | 401 | No or invalid JWT | +| `FORBIDDEN` | 403 | Valid JWT but insufficient role/scope | +| `NOT_FOUND` | 404 | Resource doesn't exist or is soft-deleted | +| `VALIDATION_ERROR` | 400 | Input fails validation | +| `CONFLICT` | 409 | Duplicate check-in, 2-manager minimum violation | +| `INTERNAL` | 500 | Unexpected server error | + +### Package Layout Rules + +- `cmd/api/main.go` is the composition root — wires dependencies, starts server +- `internal/` packages are never imported by external code +- Each `internal/` package exposes an interface and a constructor +- No circular imports — dependency direction: `graph → {auth, db, events}`, never reverse + +### Structured Logging + +```go +// Use slog everywhere +slog.Info("vendor checked in", + "vendor_id", vendorID, + "market_id", marketID, + "duration_ms", elapsed.Milliseconds(), +) + +// NEVER use fmt.Println or log.Printf in production code +``` + +### Database Patterns + +- All queries use `pgx` directly — no ORM +- All queries include `WHERE deleted_at IS NULL` for user-facing data +- All new domain tables MUST have the audit trigger attached in the migration +- Use `pgx.Pool` for connection management — never open individual connections +- Use `pgxpool.QueryRow` / `pgxpool.Query` with context for cancellation support + +--- + +## 9. React Native / TypeScript Standards + +### ESLint Rules (Enforced) + +| Rule | Why | +|------|-----| +| `@typescript-eslint/no-floating-promises` | Forgotten `await` is the #1 async bug | +| `@typescript-eslint/strict-boolean-expressions` | Prevents truthy/falsy surprises | +| `@typescript-eslint/no-unused-vars` | Dead code removal | +| React hooks rules | Enforce rules of hooks and exhaustive deps | +| No `any` type | Use proper types from `@graphql-codegen` | + +### TypeScript Configuration + +- `strict: true` — enables all strict checks +- `noUncheckedIndexedAccess: true` — forces null checks on array/object indexing + +### Component Patterns + +```typescript +// GOOD: Gluestack components with NativeWind +import { Box, Text, Button, ButtonText } from '@/components/ui'; + +export function VendorCard({ vendor }: VendorCardProps) { + return ( + + {vendor.name} + + + ); +} + +// BAD: Raw React Native components with inline styles +import { View, Text, TouchableOpacity } from 'react-native'; + +export function VendorCard({ vendor }) { + return ( + + {vendor.name} + + ); +} +``` + +--- + +## 10. Story Implementation Protocol + +When implementing any story, follow this exact sequence: + +1. **Read the story** from epics document +2. **Read referenced FRs** from the PRD for full context +3. **Read architecture decisions** relevant to the story +4. **Read UX design specs** for any UI components +5. **Write failing tests** for each acceptance criterion (Red) +6. **Implement minimum code** to pass each test (Green) +7. **Refactor** while keeping tests green +8. **Verify pre-commit passes** before committing +9. **Run full test suite** to catch regressions + +### Story Completion Checklist + +Before marking a story as done: + +- [ ] All acceptance criteria have corresponding tests +- [ ] All tests pass (unit + integration) +- [ ] Pre-commit hook passes (lint + format + type check + tests) +- [ ] Coverage meets minimum thresholds +- [ ] No `any` types introduced (TypeScript) +- [ ] No swallowed errors (Go) +- [ ] Audit trigger attached to any new domain tables +- [ ] Domain events published for any new mutations +- [ ] Soft-delete filtering applied to any new queries +- [ ] Accessibility labels on all interactive elements +- [ ] Error messages are user-friendly, not raw +- [ ] Design tokens used for all colors/spacing (no hardcoded values) + +--- + +## 11. Naming Conventions + +### Go Backend + +| Category | Convention | Example | +|----------|-----------|---------| +| Packages | lowercase, single word | `auth`, `events`, `notify` | +| Files | snake_case | `auth_middleware.go`, `event_bus.go` | +| Types/interfaces | PascalCase | `EventHandler`, `MarketResolver` | +| Functions | PascalCase (exported), camelCase (unexported) | `NewResolver()`, `validateInput()` | +| Constants | PascalCase (exported) | `MaxRetryCount`, `DefaultTimeout` | +| Test files | `*_test.go` co-located | `middleware_test.go` | +| Integration tests | `//go:build integration` tag | Tagged build constraint | +| GraphQL schema | snake_case fields, PascalCase types | `vendor_status`, `MarketProfile` | +| DB tables | snake_case, plural | `market_vendors`, `audit_log` | +| DB columns | snake_case | `created_at`, `vendor_id` | +| Migrations | `NNNNNN_description.up.sql` / `.down.sql` | `000001_create_users.up.sql` | + +### React Native Frontend + +| Category | Convention | Example | +|----------|-----------|---------| +| Components | PascalCase file + export | `VendorCard.tsx`, `MarketList.tsx` | +| Hooks | camelCase with `use` prefix | `useVendorStatus.ts` | +| Utilities/lib | camelCase files | `apollo.ts`, `firebase.ts` | +| GraphQL files | camelCase.graphql | `getMarket.graphql`, `checkIn.graphql` | +| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `API_URL` | +| Test files | `*.test.tsx` co-located | `VendorCard.test.tsx` | +| Design tokens | `--color-{name}-{scale}` | `--color-primary-500` | + +--- + +## References + +- **[petry-projects Org AGENTS.md](https://github.com/petry-projects/.github/blob/main/AGENTS.md)** — Organization-wide engineering standards (TDD, SOLID, CLEAN, DRY, DDD, KISS, YAGNI, CI gates, BMAD) +- **Agents.md** — Project-level coding standards (component organization, styling, accessibility, anti-patterns) +- **Architecture** — `_bmad-output/planning-artifacts/architecture.md` +- **PRD** — `_bmad-output/planning-artifacts/prd.md` +- **UX Design** — `_bmad-output/planning-artifacts/ux-design-specification.md` diff --git a/_bmad-output/test-artifacts/epic-1-test-strategy.md b/_bmad-output/test-artifacts/epic-1-test-strategy.md new file mode 100644 index 00000000..9acf6e5b --- /dev/null +++ b/_bmad-output/test-artifacts/epic-1-test-strategy.md @@ -0,0 +1,259 @@ +# Epic 1: Authentication & Role-Based Access — Test Strategy + +> Authored by Murat (TEA - Test Architect) during Epic 1 Sprint Planning. +> Defines the test approach, test cases, and quality gates for all 6 stories in Epic 1. + +--- + +## 1. Epic 1 Test Scope + +Epic 1 establishes the foundation: project scaffolding, authentication, role assignment, market-scoped permissions, and role-based access enforcement. Every subsequent epic depends on this one working correctly. + +**Stories:** +- 1.1a: Frontend Project Scaffolding +- 1.1b: Backend Project Scaffolding & Audit Infrastructure +- 1.2: Google & Apple Sign-In Authentication +- 1.3: Role Selection & User Record Creation +- 1.4: Market-Scoped Manager Permissions +- 1.5: Role-Based Access Enforcement Middleware + +**Risk Assessment:** HIGH — Foundation errors cascade to all subsequent epics. Auth and RBAC bugs are security vulnerabilities. + +--- + +## 2. Test Framework Setup (Stories 1.1a & 1.1b) + +### Go Backend Test Infrastructure + +| Tool | Purpose | Configuration | +|------|---------|---------------| +| `go test` | Unit + integration test runner | Standard Go tooling | +| `//go:build integration` | Tag for integration tests | Separate from unit tests in CI | +| `testcontainers-go` or local Cloud SQL | Test database | Real PostgreSQL for integration tests | +| `httptest` | HTTP handler testing | Test middleware and GraphQL endpoint | +| `golangci-lint` | Static analysis | Pre-commit + CI | + +**Test Database Strategy:** +- Unit tests: mock DB interfaces (no real database) +- Integration tests: dedicated test PostgreSQL instance with migrations applied fresh per test suite +- Each integration test runs in a transaction that is rolled back after the test + +### React Native Test Infrastructure + +| Tool | Purpose | Configuration | +|------|---------|---------------| +| Jest | Test runner | Configured via `jest.config.js` | +| React Native Testing Library | Component testing | `@testing-library/react-native` | +| `@testing-library/jest-dom` | DOM assertions | Extended matchers | +| `MockedProvider` | Apollo Client mocking | From `@apollo/client/testing` | + +**Test Verification for Scaffolding:** + +``` +Story 1.1a — Frontend scaffolding is DONE when: + [ ] Jest runs and finds test files + [ ] A smoke test renders the root layout without crashing + [ ] TypeScript compiles with zero errors (tsc --noEmit) + [ ] ESLint passes with zero warnings + [ ] GraphQL codegen generates types successfully + +Story 1.1b — Backend scaffolding is DONE when: + [ ] go test ./... passes with at least one test + [ ] A smoke test starts the HTTP server and responds to health check + [ ] gqlgen validate passes (schema is valid) + [ ] golangci-lint passes + [ ] Migrations run up and down cleanly + [ ] Audit trigger function exists and fires on test table insert + [ ] Event bus publishes and receives a test event +``` + +--- + +## 3. Story 1.2: Google & Apple Sign-In Authentication + +### Test Cases — Go Backend (Auth Middleware) + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.2.1 | Valid Google JWT accepted | Unit | Valid Firebase JWT with Google provider | Request hits auth middleware | Context populated with uid and role; request proceeds | +| 1.2.2 | Valid Apple JWT accepted | Unit | Valid Firebase JWT with Apple provider | Request hits auth middleware | Context populated with uid and role; request proceeds | +| 1.2.3 | Expired JWT rejected | Unit | Expired Firebase JWT | Request hits auth middleware | UNAUTHENTICATED error returned; request blocked | +| 1.2.4 | Malformed JWT rejected | Unit | Random string as Authorization header | Request hits auth middleware | UNAUTHENTICATED error returned | +| 1.2.5 | Missing Authorization header | Unit | No Authorization header | Request hits auth middleware | UNAUTHENTICATED error returned | +| 1.2.6 | JWT with wrong issuer | Unit | Valid JWT but not from Firebase | Request hits auth middleware | UNAUTHENTICATED error returned | +| 1.2.7 | Session variables set | Integration | Valid JWT with uid="user1", role="vendor" | Request processed by middleware | PostgreSQL session vars `app.actor_id`="user1", `app.actor_role`="vendor" set | + +### Test Cases — React Native (Auth Flow) + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.2.8 | Google sign-in button renders | Component | Login screen mounted | Screen renders | "Sign in with Google" button visible with correct accessibility label | +| 1.2.9 | Apple sign-in button renders | Component | Login screen mounted | Screen renders | "Sign in with Apple" button visible with correct accessibility label | +| 1.2.10 | Successful Google sign-in stores token | Unit (hook) | Firebase returns valid JWT | Sign-in completes | JWT stored in expo-secure-store | +| 1.2.11 | Successful sign-in configures Apollo | Unit (hook) | JWT stored in secure store | Apollo Client initialized | Authorization Bearer header set on all requests | +| 1.2.12 | Auth error shows user-friendly message | Component | Firebase returns auth error | Sign-in fails | Error message displayed (not raw error) | +| 1.2.13 | Loading state during sign-in | Component | User taps sign-in | Auth flow in progress | Loading indicator shown, button disabled | + +--- + +## 4. Story 1.3: Role Selection & User Record Creation + +### Test Cases — Go Backend + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.3.1 | Create user with customer role | Integration | Authenticated user, no existing record | `createUser` mutation with role="customer" | User record created in DB; Firebase custom claim set; role returned | +| 1.3.2 | Create user with vendor role | Integration | Authenticated user, no existing record | `createUser` mutation with role="vendor" | User record created; custom claim set | +| 1.3.3 | Create user with manager role | Integration | Authenticated user, no existing record | `createUser` mutation with role="manager" | User record created; custom claim set; recovery contact required | +| 1.3.4 | Reject duplicate user creation | Integration | User record already exists | `createUser` mutation | CONFLICT error returned | +| 1.3.5 | Reject invalid role | Unit | Authenticated user | `createUser` with role="admin" | VALIDATION_ERROR returned | +| 1.3.6 | Audit log entry created | Integration | User created successfully | Check audit_log table | Entry exists with action="user_created", actor=uid | +| 1.3.7 | User record includes required fields | Integration | User created | Query users table | id, firebase_uid, role, name, email, created_at all populated; deleted_at is NULL | + +### Test Cases — React Native + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.3.8 | Role selection screen renders three options | Component | New user (no role claim) | Screen mounts | Customer, Vendor, Manager options visible with descriptions | +| 1.3.9 | Selecting customer role navigates to customer tabs | Component | User selects Customer | Role mutation succeeds | App navigates to `(customer)/` tab layout | +| 1.3.10 | Selecting vendor role navigates to vendor tabs | Component | User selects Vendor | Role mutation succeeds | App navigates to `(vendor)/` tab layout | +| 1.3.11 | Selecting manager role navigates to manager tabs | Component | User selects Manager | Role mutation succeeds | App navigates to `(manager)/` tab layout | +| 1.3.12 | Returning user bypasses role selection | Unit (hook) | Existing user with role claim in JWT | App opens | Routed directly to role-specific tabs | +| 1.3.13 | Role selection error shows retry | Component | Network error on mutation | Role selection fails | Error message with retry option | + +--- + +## 5. Story 1.4: Market-Scoped Manager Permissions + +### Test Cases — Go Backend + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.4.1 | Assigned manager can access market | Integration | Manager assigned to Market A | Query Market A data | Data returned successfully | +| 1.4.2 | Unassigned manager blocked | Integration | Manager NOT assigned to Market B | Query Market B data | FORBIDDEN error returned | +| 1.4.3 | Two managers see same shared state | Integration | Manager1 and Manager2 both assigned to Market A | Both query dashboard | Same vendor statuses and counts returned | +| 1.4.4 | Cannot reduce below 2 managers | Integration | Market A has exactly 2 managers | Remove one manager | CONFLICT error: "Market requires minimum 2 managers" | +| 1.4.5 | Can remove manager if 3+ remain | Integration | Market A has 3 managers | Remove one manager | Success; 2 managers remain | +| 1.4.6 | market_managers junction table integrity | Integration | Manager assigned | Query junction table | Correct manager_id, market_id, created_at | +| 1.4.7 | Scope check queries market_managers | Unit | Manager with role in context | Resolver checks scope | SQL query hits market_managers table with correct manager_id | + +--- + +## 6. Story 1.5: Role-Based Access Enforcement Middleware + +### Test Cases — Go Backend + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.5.1 | Customer blocked from vendor mutation | Unit | JWT with role="customer" | `checkInVendor` mutation | FORBIDDEN | +| 1.5.2 | Customer blocked from manager query | Unit | JWT with role="customer" | `marketDashboard` query | FORBIDDEN | +| 1.5.3 | Vendor blocked from manager mutation | Unit | JWT with role="vendor" | `approveJoinRequest` mutation | FORBIDDEN | +| 1.5.4 | Vendor can access own vendor queries | Unit | JWT with role="vendor" | `myVendorProfile` query | Data returned | +| 1.5.5 | Manager can access dashboard | Unit | JWT with role="manager" | `marketDashboard` query | Data returned (with scope check) | +| 1.5.6 | Customer can access customer queries | Unit | JWT with role="customer" | `searchMarkets` query | Data returned | +| 1.5.7 | Soft-deleted records excluded | Integration | User record with deleted_at set | Any query returning users | Soft-deleted user excluded from results | +| 1.5.8 | Role enforcement at resolver level | Unit | Each resolver | Inspect role check | Every mutation/query resolver checks role before proceeding | + +### Security Edge Cases + +| ID | Test Case | Type | Given | When | Then | +|----|-----------|------|-------|------|------| +| 1.5.9 | Tampered role claim rejected | Unit | JWT with role modified after signing | Request hits middleware | UNAUTHENTICATED (signature invalid) | +| 1.5.10 | Role=empty string rejected | Unit | JWT with role="" | Request hits middleware | UNAUTHENTICATED or FORBIDDEN | +| 1.5.11 | Multiple roles not supported | Unit | JWT with role="customer,vendor" | Request hits middleware | VALIDATION_ERROR or FORBIDDEN | +| 1.5.12 | SQL injection in user ID blocked | Integration | JWT uid contains SQL injection | Query executed | Parameterized query prevents injection; no data leak | + +--- + +## 7. Cross-Cutting Test Requirements for Epic 1 + +### Audit Logging Verification + +Every write operation in Epic 1 must generate an audit log entry. Verify for each mutation: + +| Mutation | Expected Audit Entry | +|----------|---------------------| +| `createUser` | action_type="user_created", actor_id=uid, target_type="user", target_id=new_user_id | +| `assignManager` | action_type="manager_assigned", target_type="market_managers" | +| `removeManager` | action_type="manager_removed", target_type="market_managers" | + +### PostgreSQL Session Variables + +For every authenticated request, verify: +- `app.actor_id` is set to the Firebase UID +- `app.actor_role` is set to the user's role +- These values are available to the audit trigger function + +### Domain Event Publishing + +| Mutation | Expected Event | +|----------|---------------| +| `createUser` | `UserCreated{userId, role}` | +| `assignManager` | `ManagerAssigned{managerId, marketId}` | +| `removeManager` | `ManagerRemoved{managerId, marketId}` | + +Each event must be tested for: +- Event is published after successful DB write +- Event is NOT published if DB write fails +- Event handlers receive the event with correct data +- Handler failure does not roll back the DB write + +--- + +## 8. Quality Gates for Epic 1 + +### Per-Story Gates + +Before a story moves from `in-progress` to `review`: + +- [ ] All test cases from this document are implemented and passing +- [ ] Go coverage >= 80% for changed files +- [ ] React Native coverage >= 80% for changed files +- [ ] Zero ESLint warnings, zero golangci-lint issues +- [ ] TypeScript compiles cleanly (`tsc --noEmit`) +- [ ] Pre-commit hooks pass + +### Epic 1 Completion Gate + +Before Epic 1 moves to `done`: + +- [ ] All 6 stories are `done` +- [ ] Integration tests cover the full auth flow: sign-in → role selection → permission check → scope enforcement +- [ ] Security edge cases (1.5.9 - 1.5.12) all pass +- [ ] Audit logging verified for every write mutation +- [ ] Domain events verified for every write mutation +- [ ] No `any` types in TypeScript codebase +- [ ] All accessibility labels present on auth/role screens +- [ ] Skeleton screens (not spinners) for loading states + +--- + +## 9. Test Execution Order + +Stories should be implemented and tested in this order (dependencies flow downward): + +``` +1.1a (Frontend Scaffolding) ──┐ + ├── 1.2 (Authentication) ── 1.3 (Role Selection) ──┐ +1.1b (Backend Scaffolding) ──┘ │ + ├── 1.5 (RBAC Middleware) + 1.4 (Manager Scope) ──┘ +``` + +- **1.1a and 1.1b** can be developed in parallel (no dependencies) +- **1.2** requires both scaffolding stories complete +- **1.3** requires 1.2 (authentication must work for role assignment) +- **1.4** can start after 1.3 (needs user records to exist) +- **1.5** requires 1.3 and 1.4 (enforces roles and scopes) + +--- + +## 10. Recommended Test Tooling Additions + +| Tool | Purpose | When to Add | +|------|---------|-------------| +| `testcontainers-go` | Spin up PostgreSQL in Docker for integration tests | Story 1.1b | +| `go-sqlmock` | Mock pgx for unit tests (if interfaces aren't sufficient) | Story 1.2 if needed | +| `firebase-admin-go` test helpers | Create test JWTs for middleware testing | Story 1.2 | +| `msw` (Mock Service Worker) | Mock GraphQL responses in React Native tests | Story 1.2 frontend | +| Coverage reporting | `go tool cover` + Jest `--coverage` with CI thresholds | Story 1.1a/1.1b |