From f14be58c069b20bcaefbe7a60bd06b02703a0c45 Mon Sep 17 00:00:00 2001
From: neodk2004
Date: Thu, 16 Jul 2026 19:22:23 +0200
Subject: [PATCH] feat: engagement-counter denormalization, pagination fixes,
dead-code cleanup
Bundles the 2026-07-12 code-audit session (Clusters A/B/C/D partial/E/F):
denormalized Post/Advertisement reaction/comment/repost counters synced
transactionally instead of live _count queries; real cursor-based pagination
for followers/following/blocks lists; assertPetOwnership + formatRelativeTime
centralized; dead r2.ts + AWS SDK deps removed; missing DB indexes added;
account-deletion flow, mention notifications, pull-to-refresh feed, and
mobile UI/i18n fixes from the surrounding sessions.
Co-Authored-By: Claude Sonnet 5
---
.claude/rules/ecc/angular/coding-style.md | 182 ++++++++++++
.claude/rules/ecc/angular/hooks.md | 25 ++
.claude/rules/ecc/angular/patterns.md | 249 +++++++++++++++++
.claude/rules/ecc/angular/security.md | 87 ++++++
.claude/rules/ecc/angular/testing.md | 164 +++++++++++
.claude/rules/ecc/arkts/coding-style.md | 153 ++++++++++
.claude/rules/ecc/arkts/hooks.md | 135 +++++++++
.claude/rules/ecc/arkts/patterns.md | 236 ++++++++++++++++
.claude/rules/ecc/arkts/security.md | 141 ++++++++++
.claude/rules/ecc/arkts/testing.md | 126 +++++++++
.claude/rules/ecc/common/agents.md | 51 ++++
.claude/rules/ecc/common/code-review.md | 124 +++++++++
.claude/rules/ecc/common/coding-style.md | 90 ++++++
.../rules/ecc/common/development-workflow.md | 44 +++
.claude/rules/ecc/common/git-workflow.md | 24 ++
.claude/rules/ecc/common/hooks.md | 30 ++
.claude/rules/ecc/common/patterns.md | 31 +++
.claude/rules/ecc/common/performance.md | 55 ++++
.claude/rules/ecc/common/security.md | 29 ++
.claude/rules/ecc/common/testing.md | 57 ++++
.claude/rules/ecc/cpp/coding-style.md | 44 +++
.claude/rules/ecc/cpp/hooks.md | 39 +++
.claude/rules/ecc/cpp/patterns.md | 51 ++++
.claude/rules/ecc/cpp/security.md | 51 ++++
.claude/rules/ecc/cpp/testing.md | 44 +++
.claude/rules/ecc/csharp/coding-style.md | 72 +++++
.claude/rules/ecc/csharp/hooks.md | 25 ++
.claude/rules/ecc/csharp/patterns.md | 50 ++++
.claude/rules/ecc/csharp/security.md | 58 ++++
.claude/rules/ecc/csharp/testing.md | 46 +++
.claude/rules/ecc/dart/coding-style.md | 159 +++++++++++
.claude/rules/ecc/dart/hooks.md | 66 +++++
.claude/rules/ecc/dart/patterns.md | 261 ++++++++++++++++++
.claude/rules/ecc/dart/security.md | 135 +++++++++
.claude/rules/ecc/dart/testing.md | 215 +++++++++++++++
.claude/rules/ecc/fsharp/coding-style.md | 112 ++++++++
.claude/rules/ecc/fsharp/hooks.md | 26 ++
.claude/rules/ecc/fsharp/patterns.md | 111 ++++++++
.claude/rules/ecc/fsharp/security.md | 76 +++++
.claude/rules/ecc/fsharp/testing.md | 62 +++++
.claude/rules/ecc/golang/coding-style.md | 32 +++
.claude/rules/ecc/golang/hooks.md | 17 ++
.claude/rules/ecc/golang/patterns.md | 45 +++
.claude/rules/ecc/golang/security.md | 34 +++
.claude/rules/ecc/golang/testing.md | 31 +++
.claude/rules/ecc/java/coding-style.md | 114 ++++++++
.claude/rules/ecc/java/hooks.md | 18 ++
.claude/rules/ecc/java/patterns.md | 147 ++++++++++
.claude/rules/ecc/java/security.md | 101 +++++++
.claude/rules/ecc/java/testing.md | 133 +++++++++
.claude/rules/ecc/kotlin/coding-style.md | 86 ++++++
.claude/rules/ecc/kotlin/hooks.md | 17 ++
.claude/rules/ecc/kotlin/patterns.md | 146 ++++++++++
.claude/rules/ecc/kotlin/security.md | 82 ++++++
.claude/rules/ecc/kotlin/testing.md | 128 +++++++++
.claude/rules/ecc/nuxt/coding-style.md | 47 ++++
.claude/rules/ecc/nuxt/hooks.md | 39 +++
.claude/rules/ecc/nuxt/patterns.md | 54 ++++
.claude/rules/ecc/nuxt/security.md | 48 ++++
.claude/rules/ecc/nuxt/testing.md | 49 ++++
.claude/rules/ecc/perl/coding-style.md | 46 +++
.claude/rules/ecc/perl/hooks.md | 22 ++
.claude/rules/ecc/perl/patterns.md | 76 +++++
.claude/rules/ecc/perl/security.md | 69 +++++
.claude/rules/ecc/perl/testing.md | 54 ++++
.claude/rules/ecc/php/coding-style.md | 40 +++
.claude/rules/ecc/php/hooks.md | 24 ++
.claude/rules/ecc/php/patterns.md | 33 +++
.claude/rules/ecc/php/security.md | 37 +++
.claude/rules/ecc/php/testing.md | 39 +++
.claude/rules/ecc/python/coding-style.md | 42 +++
.claude/rules/ecc/python/fastapi.md | 58 ++++
.claude/rules/ecc/python/hooks.md | 19 ++
.claude/rules/ecc/python/patterns.md | 39 +++
.claude/rules/ecc/python/security.md | 30 ++
.claude/rules/ecc/python/testing.md | 38 +++
.claude/rules/ecc/react/coding-style.md | 109 ++++++++
.claude/rules/ecc/react/hooks.md | 187 +++++++++++++
.claude/rules/ecc/react/patterns.md | 194 +++++++++++++
.claude/rules/ecc/react/security.md | 180 ++++++++++++
.claude/rules/ecc/react/testing.md | 208 ++++++++++++++
.claude/rules/ecc/ruby/coding-style.md | 46 +++
.claude/rules/ecc/ruby/hooks.md | 37 +++
.claude/rules/ecc/ruby/patterns.md | 44 +++
.claude/rules/ecc/ruby/security.md | 51 ++++
.claude/rules/ecc/ruby/testing.md | 51 ++++
.claude/rules/ecc/rust/coding-style.md | 151 ++++++++++
.claude/rules/ecc/rust/hooks.md | 16 ++
.claude/rules/ecc/rust/patterns.md | 168 +++++++++++
.claude/rules/ecc/rust/security.md | 141 ++++++++++
.claude/rules/ecc/rust/testing.md | 154 +++++++++++
.claude/rules/ecc/swift/coding-style.md | 47 ++++
.claude/rules/ecc/swift/hooks.md | 20 ++
.claude/rules/ecc/swift/patterns.md | 66 +++++
.claude/rules/ecc/swift/security.md | 33 +++
.claude/rules/ecc/swift/testing.md | 45 +++
.claude/rules/ecc/typescript/coding-style.md | 199 +++++++++++++
.claude/rules/ecc/typescript/hooks.md | 22 ++
.claude/rules/ecc/typescript/patterns.md | 52 ++++
.claude/rules/ecc/typescript/security.md | 28 ++
.claude/rules/ecc/typescript/testing.md | 18 ++
.claude/rules/ecc/vue/coding-style.md | 54 ++++
.claude/rules/ecc/vue/hooks.md | 45 +++
.claude/rules/ecc/vue/patterns.md | 56 ++++
.claude/rules/ecc/vue/security.md | 46 +++
.claude/rules/ecc/vue/testing.md | 53 ++++
.claude/rules/ecc/web/coding-style.md | 96 +++++++
.claude/rules/ecc/web/design-quality.md | 63 +++++
.claude/rules/ecc/web/hooks.md | 129 +++++++++
.claude/rules/ecc/web/patterns.md | 79 ++++++
.claude/rules/ecc/web/performance.md | 64 +++++
.claude/rules/ecc/web/security.md | 57 ++++
.claude/rules/ecc/web/testing.md | 55 ++++
.claude/settings.json | 7 +
.claude/settings.local.json | 8 +-
.gitignore | 4 +
CLAUDE.md | 248 ++++++-----------
DEBUG-List.md | 12 +-
Supabase-Error.md | 42 +++
messages/de.json | 6 +
messages/en.json | 6 +
mobil.md | 75 +++++
package.json | 5 +-
prisma/backfill-counters.sql | 14 +
prisma/schema.prisma | 27 +-
src/__tests__/admin.test.ts | 97 +++++++
src/__tests__/ads.test.ts | 176 ++++++++++++
src/__tests__/blocks.test.ts | 7 +-
src/__tests__/comments.test.ts | 17 ++
src/__tests__/follows.test.ts | 10 +-
src/__tests__/helpers/prisma-mock.ts | 83 ++++++
src/__tests__/media-upload.test.ts | 32 ++-
src/__tests__/pet-profile.test.ts | 2 +
src/__tests__/reactions.test.ts | 17 ++
src/__tests__/reposts.test.ts | 41 +++
src/app/(app)/explore/page.tsx | 5 +-
src/app/(app)/feed/page.tsx | 17 +-
src/app/(app)/invite/page.tsx | 54 +++-
src/app/(app)/messages/page.tsx | 19 +-
src/app/(app)/notifications/page.tsx | 27 +-
src/app/(app)/pets/[petId]/followers/page.tsx | 2 +-
src/app/(app)/pets/[petId]/following/page.tsx | 2 +-
src/app/(app)/pets/[petId]/page.tsx | 2 +-
src/app/(app)/pets/page.tsx | 3 +
src/app/api/account/delete/route.ts | 68 +++++
src/app/api/cron/trim-feeds/route.ts | 31 +++
src/app/p/[secret]/invites/page.tsx | 55 +++-
src/app/p/[secret]/posts/page.tsx | 6 +-
src/app/p/[secret]/reports/page.tsx | 4 +-
.../account/DeleteAccountDialog.tsx | 192 +++++++++++++
src/components/feed/AdCard.tsx | 18 +-
src/components/feed/FeedHeader.tsx | 33 +++
src/components/feed/FeedPullToRefresh.tsx | 98 +++++++
src/components/feed/PostCard.tsx | 24 +-
src/components/feed/PostDetailDialog.tsx | 27 +-
src/components/messages/MessageThread.tsx | 2 +-
src/components/ui/mention-textarea.tsx | 2 +-
src/lib/assert-pet-ownership.ts | 23 ++
src/lib/format-relative-time.ts | 36 +++
src/lib/mention-helpers.ts | 54 ++++
src/lib/milestone-thresholds.ts | 31 +--
src/lib/r2.ts | 43 ---
src/lib/supabase-storage.ts | 12 +
src/trpc/routers/admin.ts | 77 +++++-
src/trpc/routers/ads.ts | 101 ++++---
src/trpc/routers/blocks.ts | 43 ++-
src/trpc/routers/comments.ts | 42 +--
src/trpc/routers/explore.ts | 5 +-
src/trpc/routers/feed.ts | 27 +-
src/trpc/routers/follows.ts | 74 +++--
src/trpc/routers/health.ts | 13 +-
src/trpc/routers/media.ts | 22 +-
src/trpc/routers/messages.ts | 7 +-
src/trpc/routers/milestones.ts | 15 +-
src/trpc/routers/pets.ts | 24 +-
src/trpc/routers/posts.ts | 20 +-
src/trpc/routers/reactions.ts | 38 +--
src/trpc/routers/reports.ts | 23 +-
src/trpc/routers/reposts.ts | 65 +++--
src/trpc/routers/stories.ts | 16 +-
src/trpc/routers/videos.ts | 10 +-
vercel.json | 4 +
182 files changed, 10562 insertions(+), 724 deletions(-)
create mode 100644 .claude/rules/ecc/angular/coding-style.md
create mode 100644 .claude/rules/ecc/angular/hooks.md
create mode 100644 .claude/rules/ecc/angular/patterns.md
create mode 100644 .claude/rules/ecc/angular/security.md
create mode 100644 .claude/rules/ecc/angular/testing.md
create mode 100644 .claude/rules/ecc/arkts/coding-style.md
create mode 100644 .claude/rules/ecc/arkts/hooks.md
create mode 100644 .claude/rules/ecc/arkts/patterns.md
create mode 100644 .claude/rules/ecc/arkts/security.md
create mode 100644 .claude/rules/ecc/arkts/testing.md
create mode 100644 .claude/rules/ecc/common/agents.md
create mode 100644 .claude/rules/ecc/common/code-review.md
create mode 100644 .claude/rules/ecc/common/coding-style.md
create mode 100644 .claude/rules/ecc/common/development-workflow.md
create mode 100644 .claude/rules/ecc/common/git-workflow.md
create mode 100644 .claude/rules/ecc/common/hooks.md
create mode 100644 .claude/rules/ecc/common/patterns.md
create mode 100644 .claude/rules/ecc/common/performance.md
create mode 100644 .claude/rules/ecc/common/security.md
create mode 100644 .claude/rules/ecc/common/testing.md
create mode 100644 .claude/rules/ecc/cpp/coding-style.md
create mode 100644 .claude/rules/ecc/cpp/hooks.md
create mode 100644 .claude/rules/ecc/cpp/patterns.md
create mode 100644 .claude/rules/ecc/cpp/security.md
create mode 100644 .claude/rules/ecc/cpp/testing.md
create mode 100644 .claude/rules/ecc/csharp/coding-style.md
create mode 100644 .claude/rules/ecc/csharp/hooks.md
create mode 100644 .claude/rules/ecc/csharp/patterns.md
create mode 100644 .claude/rules/ecc/csharp/security.md
create mode 100644 .claude/rules/ecc/csharp/testing.md
create mode 100644 .claude/rules/ecc/dart/coding-style.md
create mode 100644 .claude/rules/ecc/dart/hooks.md
create mode 100644 .claude/rules/ecc/dart/patterns.md
create mode 100644 .claude/rules/ecc/dart/security.md
create mode 100644 .claude/rules/ecc/dart/testing.md
create mode 100644 .claude/rules/ecc/fsharp/coding-style.md
create mode 100644 .claude/rules/ecc/fsharp/hooks.md
create mode 100644 .claude/rules/ecc/fsharp/patterns.md
create mode 100644 .claude/rules/ecc/fsharp/security.md
create mode 100644 .claude/rules/ecc/fsharp/testing.md
create mode 100644 .claude/rules/ecc/golang/coding-style.md
create mode 100644 .claude/rules/ecc/golang/hooks.md
create mode 100644 .claude/rules/ecc/golang/patterns.md
create mode 100644 .claude/rules/ecc/golang/security.md
create mode 100644 .claude/rules/ecc/golang/testing.md
create mode 100644 .claude/rules/ecc/java/coding-style.md
create mode 100644 .claude/rules/ecc/java/hooks.md
create mode 100644 .claude/rules/ecc/java/patterns.md
create mode 100644 .claude/rules/ecc/java/security.md
create mode 100644 .claude/rules/ecc/java/testing.md
create mode 100644 .claude/rules/ecc/kotlin/coding-style.md
create mode 100644 .claude/rules/ecc/kotlin/hooks.md
create mode 100644 .claude/rules/ecc/kotlin/patterns.md
create mode 100644 .claude/rules/ecc/kotlin/security.md
create mode 100644 .claude/rules/ecc/kotlin/testing.md
create mode 100644 .claude/rules/ecc/nuxt/coding-style.md
create mode 100644 .claude/rules/ecc/nuxt/hooks.md
create mode 100644 .claude/rules/ecc/nuxt/patterns.md
create mode 100644 .claude/rules/ecc/nuxt/security.md
create mode 100644 .claude/rules/ecc/nuxt/testing.md
create mode 100644 .claude/rules/ecc/perl/coding-style.md
create mode 100644 .claude/rules/ecc/perl/hooks.md
create mode 100644 .claude/rules/ecc/perl/patterns.md
create mode 100644 .claude/rules/ecc/perl/security.md
create mode 100644 .claude/rules/ecc/perl/testing.md
create mode 100644 .claude/rules/ecc/php/coding-style.md
create mode 100644 .claude/rules/ecc/php/hooks.md
create mode 100644 .claude/rules/ecc/php/patterns.md
create mode 100644 .claude/rules/ecc/php/security.md
create mode 100644 .claude/rules/ecc/php/testing.md
create mode 100644 .claude/rules/ecc/python/coding-style.md
create mode 100644 .claude/rules/ecc/python/fastapi.md
create mode 100644 .claude/rules/ecc/python/hooks.md
create mode 100644 .claude/rules/ecc/python/patterns.md
create mode 100644 .claude/rules/ecc/python/security.md
create mode 100644 .claude/rules/ecc/python/testing.md
create mode 100644 .claude/rules/ecc/react/coding-style.md
create mode 100644 .claude/rules/ecc/react/hooks.md
create mode 100644 .claude/rules/ecc/react/patterns.md
create mode 100644 .claude/rules/ecc/react/security.md
create mode 100644 .claude/rules/ecc/react/testing.md
create mode 100644 .claude/rules/ecc/ruby/coding-style.md
create mode 100644 .claude/rules/ecc/ruby/hooks.md
create mode 100644 .claude/rules/ecc/ruby/patterns.md
create mode 100644 .claude/rules/ecc/ruby/security.md
create mode 100644 .claude/rules/ecc/ruby/testing.md
create mode 100644 .claude/rules/ecc/rust/coding-style.md
create mode 100644 .claude/rules/ecc/rust/hooks.md
create mode 100644 .claude/rules/ecc/rust/patterns.md
create mode 100644 .claude/rules/ecc/rust/security.md
create mode 100644 .claude/rules/ecc/rust/testing.md
create mode 100644 .claude/rules/ecc/swift/coding-style.md
create mode 100644 .claude/rules/ecc/swift/hooks.md
create mode 100644 .claude/rules/ecc/swift/patterns.md
create mode 100644 .claude/rules/ecc/swift/security.md
create mode 100644 .claude/rules/ecc/swift/testing.md
create mode 100644 .claude/rules/ecc/typescript/coding-style.md
create mode 100644 .claude/rules/ecc/typescript/hooks.md
create mode 100644 .claude/rules/ecc/typescript/patterns.md
create mode 100644 .claude/rules/ecc/typescript/security.md
create mode 100644 .claude/rules/ecc/typescript/testing.md
create mode 100644 .claude/rules/ecc/vue/coding-style.md
create mode 100644 .claude/rules/ecc/vue/hooks.md
create mode 100644 .claude/rules/ecc/vue/patterns.md
create mode 100644 .claude/rules/ecc/vue/security.md
create mode 100644 .claude/rules/ecc/vue/testing.md
create mode 100644 .claude/rules/ecc/web/coding-style.md
create mode 100644 .claude/rules/ecc/web/design-quality.md
create mode 100644 .claude/rules/ecc/web/hooks.md
create mode 100644 .claude/rules/ecc/web/patterns.md
create mode 100644 .claude/rules/ecc/web/performance.md
create mode 100644 .claude/rules/ecc/web/security.md
create mode 100644 .claude/rules/ecc/web/testing.md
create mode 100644 .claude/settings.json
create mode 100644 Supabase-Error.md
create mode 100644 mobil.md
create mode 100644 prisma/backfill-counters.sql
create mode 100644 src/__tests__/admin.test.ts
create mode 100644 src/__tests__/ads.test.ts
create mode 100644 src/app/api/account/delete/route.ts
create mode 100644 src/app/api/cron/trim-feeds/route.ts
create mode 100644 src/components/account/DeleteAccountDialog.tsx
create mode 100644 src/components/feed/FeedHeader.tsx
create mode 100644 src/components/feed/FeedPullToRefresh.tsx
create mode 100644 src/lib/assert-pet-ownership.ts
create mode 100644 src/lib/format-relative-time.ts
create mode 100644 src/lib/mention-helpers.ts
delete mode 100644 src/lib/r2.ts
diff --git a/.claude/rules/ecc/angular/coding-style.md b/.claude/rules/ecc/angular/coding-style.md
new file mode 100644
index 0000000..bed3986
--- /dev/null
+++ b/.claude/rules/ecc/angular/coding-style.md
@@ -0,0 +1,182 @@
+---
+paths:
+ - "**/*.component.ts"
+ - "**/*.component.html"
+ - "**/*.service.ts"
+ - "**/*.directive.ts"
+ - "**/*.pipe.ts"
+ - "**/*.guard.ts"
+ - "**/*.resolver.ts"
+ - "**/*.module.ts"
+---
+# Angular Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Angular specific content.
+
+## Version Awareness
+
+Always check the project's Angular version before writing code — features differ significantly between versions. Run `ng version` or inspect `package.json`. When creating a new project, do not pin a version unless the user specifies one.
+
+After generating or modifying Angular code, always run `ng build` to catch errors before finishing.
+
+## File Naming
+
+Follow Angular CLI conventions — one artifact per file:
+
+- `user-profile.component.ts` + `user-profile.component.html` + `user-profile.component.spec.ts`
+- `user.service.ts`, `auth.guard.ts`, `date-format.pipe.ts`
+- Feature folders: `features/users/`, `features/auth/`
+- Generate with the CLI: `ng generate component features/users/user-card`
+
+## Components
+
+Prefer standalone components (v17+ default). Use `OnPush` change detection on all new components.
+
+```typescript
+@Component({
+ selector: 'app-user-card',
+ standalone: true,
+ imports: [RouterModule],
+ templateUrl: './user-card.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserCardComponent {
+ user = input.required();
+ select = output();
+}
+```
+
+## Dependency Injection
+
+Use `inject()` over constructor injection. Keep constructors empty or remove them entirely.
+
+```typescript
+// CORRECT
+@Injectable({ providedIn: 'root' })
+export class UserService {
+ private http = inject(HttpClient);
+ private router = inject(Router);
+}
+
+// WRONG: Constructor injection is verbose and harder to tree-shake
+constructor(private http: HttpClient, private router: Router) {}
+```
+
+Use `InjectionToken` for non-class dependencies:
+
+```typescript
+const API_URL = new InjectionToken('API_URL');
+
+// Provide:
+{ provide: API_URL, useValue: 'https://api.example.com' }
+
+// Consume:
+private apiUrl = inject(API_URL);
+```
+
+## Signals
+
+### Core Primitives
+
+```typescript
+count = signal(0);
+doubled = computed(() => this.count() * 2);
+
+increment() {
+ this.count.update(n => n + 1);
+}
+```
+
+### `linkedSignal` — Writable Derived State
+
+Use `linkedSignal` when a signal must reset or adapt when a source changes, but also be independently writable:
+
+```typescript
+selectedOption = linkedSignal(() => this.options()[0]);
+// Resets to first option when options changes, but user can override
+```
+
+### `resource` — Async Data into Signals
+
+Use `resource()` to fetch async data reactively without manual subscriptions:
+
+```typescript
+userResource = resource({
+ request: () => ({ id: this.userId() }),
+ loader: ({ request }) => fetch(`/api/users/${request.id}`).then(r => r.json()),
+});
+
+// Access: userResource.value(), userResource.isLoading(), userResource.error()
+```
+
+### `effect` Usage
+
+Use `effect()` only for side effects that must react to signal changes (logging, third-party DOM manipulation). Never use effects to synchronize signals — use `computed` or `linkedSignal` instead. For DOM work after render, use `afterRenderEffect`.
+
+```typescript
+// CORRECT: Side effect
+effect(() => console.log('User changed:', this.user()));
+
+// WRONG: Use computed instead
+effect(() => { this.fullName.set(`${this.first()} ${this.last()}`); });
+```
+
+## Templates
+
+Use v17+ block syntax. Always provide `track` in `@for`:
+
+```html
+@for (item of items(); track item.id) {
+
+}
+
+@if (isLoading()) {
+
+} @else if (error()) {
+
+} @else {
+
+}
+```
+
+No logic in templates beyond simple conditionals — move to component methods or pipes.
+
+## Forms
+
+Choose the form strategy that matches the project's existing approach:
+
+- **Signal Forms** (v21+): Preferred for new projects on v21+. Signal-based form state.
+- **Reactive Forms**: `FormBuilder` + `FormGroup` + `FormControl`. Best for complex forms with dynamic validation.
+- **Template-Driven Forms**: `ngModel`. Suitable for simple forms only.
+
+```typescript
+// Reactive Forms — standard approach for most apps
+export class LoginComponent {
+ private fb = inject(FormBuilder);
+
+ form = this.fb.group({
+ email: ['', [Validators.required, Validators.email]],
+ password: ['', [Validators.required, Validators.minLength(8)]],
+ });
+
+ submit() {
+ if (this.form.valid) {
+ // use this.form.value
+ }
+ }
+}
+```
+
+## Component Styles
+
+Use component-level styles with `ViewEncapsulation.Emulated` (default). Avoid `ViewEncapsulation.None` unless building a design system that intentionally bleeds styles.
+
+- Scope styles to the component — do not use global class names inside component stylesheets
+- Use `:host` for host element styling
+- Prefer CSS custom properties for themeable values
+
+## Change Detection
+
+- Default to `ChangeDetectionStrategy.OnPush` on all new components
+- Signals and `async` pipe handle detection automatically — avoid `markForCheck()` and `detectChanges()`
+- Never mutate `@Input()` objects in place when using OnPush
diff --git a/.claude/rules/ecc/angular/hooks.md b/.claude/rules/ecc/angular/hooks.md
new file mode 100644
index 0000000..987a498
--- /dev/null
+++ b/.claude/rules/ecc/angular/hooks.md
@@ -0,0 +1,25 @@
+---
+paths:
+ - "**/*.component.ts"
+ - "**/*.component.html"
+ - "**/*.service.ts"
+ - "**/*.directive.ts"
+ - "**/*.pipe.ts"
+ - "**/*.spec.ts"
+---
+# Angular Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Angular specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **Prettier**: Auto-format `.ts` and `.html` files after edit
+- **ESLint / ng lint**: Run `ng lint` after editing Angular source files to catch decorator misuse, template errors, and style violations
+- **TypeScript check**: Run `tsc --noEmit` after editing `.ts` files
+- **Build check**: Run `ng build` after generating or significantly changing Angular code to catch template and type errors early
+
+## Stop Hooks
+
+- **Lint audit**: Run `ng lint` across modified files before session ends to catch any outstanding violations
diff --git a/.claude/rules/ecc/angular/patterns.md b/.claude/rules/ecc/angular/patterns.md
new file mode 100644
index 0000000..c703531
--- /dev/null
+++ b/.claude/rules/ecc/angular/patterns.md
@@ -0,0 +1,249 @@
+---
+paths:
+ - "**/*.component.ts"
+ - "**/*.component.html"
+ - "**/*.service.ts"
+ - "**/*.store.ts"
+ - "**/*.routes.ts"
+---
+# Angular Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Angular specific content.
+
+## Smart / Dumb Component Split
+
+Smart (container) components own data fetching and state. Dumb (presentational) components receive inputs and emit outputs only — no service injection.
+
+```typescript
+// Smart — owns data
+@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
+export class UserPageComponent {
+ private userService = inject(UserService);
+ user = toSignal(this.userService.getUser(this.userId));
+}
+```
+
+```html
+
+
+```
+
+## Service Layer
+
+Services own all data access and business logic. Components delegate — no `HttpClient` in components.
+
+```typescript
+@Injectable({ providedIn: 'root' })
+export class UserService {
+ private http = inject(HttpClient);
+
+ getUsers(): Observable {
+ return this.http.get('/api/users');
+ }
+}
+```
+
+## Async Data with `resource`
+
+Use `resource()` for reactive async fetching. Prefer over manual RxJS pipelines for simple data loading:
+
+```typescript
+export class UserDetailComponent {
+ userId = input.required();
+
+ userResource = resource({
+ request: () => ({ id: this.userId() }),
+ loader: ({ request }) =>
+ firstValueFrom(inject(UserService).getUser(request.id)),
+ });
+}
+```
+
+Access state: `userResource.value()`, `userResource.isLoading()`, `userResource.error()`, `userResource.reload()`.
+
+## Signal State Patterns
+
+```typescript
+// Local mutable state
+count = signal(0);
+
+// Derived (never duplicated)
+doubled = computed(() => this.count() * 2);
+
+// Writable derived state that resets with source
+selectedItem = linkedSignal(() => this.items()[0]);
+
+// Bridge Observable to signal
+users = toSignal(this.userService.getUsers(), { initialValue: [] });
+```
+
+Never store derived values in separate signals — use `computed`. Never use `effect` to sync signals — use `computed` or `linkedSignal`.
+
+## Subscription Cleanup
+
+Use `takeUntilDestroyed()` for all manual subscriptions. Never use manual `ngOnDestroy` + `Subject` + `takeUntil` on new code.
+
+```typescript
+export class UserComponent {
+ private destroyRef = inject(DestroyRef);
+
+ ngOnInit() {
+ this.userService.updates$
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(update => this.handleUpdate(update));
+ }
+}
+```
+
+## Routing
+
+### Route Definition
+
+```typescript
+// app.routes.ts
+export const routes: Routes = [
+ { path: '', component: HomeComponent },
+ {
+ path: 'admin',
+ canMatch: [authGuard], // CanMatch prevents loading the chunk at all
+ loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
+ },
+ {
+ path: 'users/:id',
+ resolve: { user: userResolver },
+ component: UserDetailComponent,
+ },
+];
+```
+
+- Use `canMatch` over `canActivate` when the route module should not load for unauthorized users
+- Lazy-load all feature modules with `loadChildren`
+- Pre-fetch data with `resolve` to avoid loading states in components
+
+### Functional Guards
+
+```typescript
+export const authGuard: CanActivateFn = () => {
+ const auth = inject(AuthService);
+ return auth.isAuthenticated()
+ ? true
+ : inject(Router).createUrlTree(['/login']);
+};
+```
+
+### Data Resolvers
+
+```typescript
+export const userResolver: ResolveFn = (route) => {
+ return inject(UserService).getUser(route.paramMap.get('id')!);
+};
+```
+
+### View Transitions
+
+Enable smooth route transitions with the View Transitions API:
+
+```typescript
+// app.config.ts
+provideRouter(routes, withViewTransitions())
+```
+
+## Dependency Injection Patterns
+
+### Scoped Providers
+
+Provide services at component or route level when they should not be singletons:
+
+```typescript
+@Component({
+ providers: [UserEditService], // scoped to this component subtree
+})
+export class UserEditComponent {}
+```
+
+### `InjectionToken`
+
+```typescript
+export const CONFIG = new InjectionToken('APP_CONFIG');
+
+// In providers:
+{ provide: CONFIG, useValue: appConfig }
+{ provide: CONFIG, useFactory: () => loadConfig(), deps: [] }
+
+// Consume:
+private config = inject(CONFIG);
+```
+
+### `viewProviders` vs `providers`
+
+- `providers`: Available to the component and all its content children
+- `viewProviders`: Available only to the component's own view (not projected content)
+
+## HTTP Interceptors
+
+Use functional interceptors (v15+) for auth, error handling, and retries:
+
+```typescript
+export const authInterceptor: HttpInterceptorFn = (req, next) => {
+ const token = inject(AuthService).token();
+ if (!token) return next(req);
+ return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
+};
+```
+
+Register in `app.config.ts`:
+
+```typescript
+provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
+```
+
+## RxJS Operators
+
+- `switchMap` — search, navigation (cancels previous)
+- `mergeMap` — independent parallel requests
+- `exhaustMap` — form submissions (ignores until complete)
+- Always handle errors with `catchError` — never let streams die silently
+
+```typescript
+search$ = this.query$.pipe(
+ debounceTime(300),
+ distinctUntilChanged(),
+ switchMap(q => this.service.search(q).pipe(catchError(() => of([])))),
+);
+```
+
+## Forms
+
+Match the project's existing form strategy. For new v21+ apps, prefer signal forms.
+
+```typescript
+// Reactive Forms — standard for complex forms
+export class UserFormComponent {
+ private fb = inject(FormBuilder);
+
+ form = this.fb.group({
+ name: ['', Validators.required],
+ email: ['', [Validators.required, Validators.email]],
+ });
+}
+```
+
+## Rendering Strategies
+
+- **CSR** (default): Standard SPA
+- **SSR + Hydration**: `ng add @angular/ssr` — improves FCP and SEO
+- **SSG (Prerendering)**: Static pages at build time for content-heavy routes
+
+When using SSR, avoid `window`, `document`, `localStorage` directly — use `isPlatformBrowser` or `DOCUMENT` token.
+
+## Accessibility
+
+Use Angular CDK for headless, accessible components (Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid). Style ARIA attributes rather than managing them manually:
+
+```css
+[aria-selected="true"] { background: var(--color-selected); }
+```
+
+## Skill Reference
+
+See skill: `angular-developer` for deep guidance on signals, forms, routing, DI, SSR, and accessibility patterns.
diff --git a/.claude/rules/ecc/angular/security.md b/.claude/rules/ecc/angular/security.md
new file mode 100644
index 0000000..167e5eb
--- /dev/null
+++ b/.claude/rules/ecc/angular/security.md
@@ -0,0 +1,87 @@
+---
+paths:
+ - "**/*.component.ts"
+ - "**/*.component.html"
+ - "**/*.service.ts"
+ - "**/*.interceptor.ts"
+---
+# Angular Security
+
+> This file extends [common/security.md](../common/security.md) with Angular specific content.
+
+## XSS Prevention
+
+Angular auto-sanitizes bound values. Never bypass the sanitizer on user-controlled input.
+
+```typescript
+// WRONG: Bypasses sanitization — XSS risk
+this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(userInput);
+
+// CORRECT: Sanitize explicitly before trusting
+this.safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, userInput);
+```
+
+- Never use `bypassSecurityTrust*` methods without a documented, reviewed reason
+- Avoid `[innerHTML]` with untrusted content — use `innerText` or a sanitizing pipe
+- Never bind `[href]` to user input — Angular does not block `javascript:` URLs in all contexts
+- Never construct template strings from user data
+
+## HTTP Security
+
+Use `HttpClient` exclusively — never raw `fetch()` or `XHR` unless no alternative exists.
+
+```typescript
+// WRONG: Bypasses interceptors (auth headers, error handling, logging)
+const res = await fetch('/api/users');
+
+// CORRECT
+users$ = this.http.get('/api/users');
+```
+
+- Attach auth tokens via interceptors — never hardcode in individual service calls
+- Type and validate API responses — treat external data as `unknown` at the boundary
+- Never log HTTP responses that may contain tokens, PII, or credentials
+
+## Secret Management
+
+```typescript
+// WRONG: Hardcoded secret in source
+const apiKey = 'sk-live-xxxx';
+
+// CORRECT: Injected via environment
+import { environment } from '../environments/environment';
+const apiKey = environment.apiKey;
+```
+
+- Treat `environment.ts` as a config shape — never store real secrets in source-controlled environment files
+- Inject production secrets via CI/CD (environment variables, secret managers)
+
+## Route Guards
+
+Every authenticated or role-restricted route must have a guard. Never rely on hiding UI elements alone.
+
+```typescript
+{
+ path: 'admin',
+ canMatch: [authGuard, roleGuard('admin')],
+ loadChildren: () => import('./admin/admin.routes'),
+}
+```
+
+Use `canMatch` for sensitive routes — it prevents the route module from loading at all for unauthorized users.
+
+## SSR Security
+
+When using Angular SSR:
+
+- Never expose server-side environment variables to the client via `TransferState` unless they are intentionally public
+- Sanitize all inputs before server-side rendering — DOM-based XSS can occur server-side too
+- Avoid `window`, `document`, `localStorage` on the server — gate with `isPlatformBrowser` or inject via `DOCUMENT` token
+
+## Content Security Policy
+
+Configure CSP headers server-side. Avoid `unsafe-inline` in `script-src`. When using SSR with inline scripts, use nonces via Angular's CSP support.
+
+## Agent Support
+
+- Use **security-reviewer** skill for comprehensive security audits
diff --git a/.claude/rules/ecc/angular/testing.md b/.claude/rules/ecc/angular/testing.md
new file mode 100644
index 0000000..f2f4d93
--- /dev/null
+++ b/.claude/rules/ecc/angular/testing.md
@@ -0,0 +1,164 @@
+---
+paths:
+ - "**/*.spec.ts"
+ - "**/*.test.ts"
+---
+# Angular Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Angular specific content.
+
+## Test Runner
+
+Use the test runner configured by the project. Check `angular.json` and `package.json`; Angular projects commonly use Vitest, Jest, or Jasmine + Karma.
+
+```bash
+ng test # watch mode
+ng test --no-watch # CI mode
+```
+
+## TestBed Setup
+
+For standalone components, import the component directly. Call `compileComponents()` for components with external templates.
+
+```typescript
+describe('UserCardComponent', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UserCardComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(UserCardComponent);
+ });
+});
+```
+
+## Signal Inputs
+
+Set signal-based inputs via `fixture.componentRef.setInput()`:
+
+```typescript
+fixture.componentRef.setInput('user', mockUser);
+fixture.detectChanges();
+```
+
+## Component Harnesses
+
+Prefer Angular CDK component harnesses over direct DOM queries for UI interaction. Harnesses are more resilient to markup changes.
+
+```typescript
+import { HarnessLoader } from '@angular/cdk/testing';
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { MatButtonHarness } from '@angular/material/button/testing';
+
+let loader: HarnessLoader;
+
+beforeEach(() => {
+ loader = TestbedHarnessEnvironment.loader(fixture);
+});
+
+it('triggers save on button click', async () => {
+ const button = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
+ await button.click();
+ expect(saveSpy).toHaveBeenCalled();
+});
+```
+
+## Router Testing
+
+Use `RouterTestingHarness` for components that depend on the router:
+
+```typescript
+import { RouterTestingHarness } from '@angular/router/testing';
+
+it('renders user on navigation', async () => {
+ const harness = await RouterTestingHarness.create();
+ const component = await harness.navigateByUrl('/users/1', UserDetailComponent);
+ expect(component.userId()).toBe('1');
+});
+```
+
+## Async Testing
+
+Use `fakeAsync` + `tick` for controlled async. Use `waitForAsync` for real async with `fixture.whenStable()`.
+
+```typescript
+it('loads user after delay', fakeAsync(() => {
+ const service = TestBed.inject(UserService);
+ vi.spyOn(service, 'getUser').mockReturnValue(of(mockUser));
+
+ fixture.detectChanges();
+ tick();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.querySelector('.name').textContent).toBe(mockUser.name);
+}));
+```
+
+## HTTP Testing
+
+```typescript
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { HttpTestingController } from '@angular/common/http/testing';
+
+beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideHttpClient(), provideHttpClientTesting()],
+ });
+ httpMock = TestBed.inject(HttpTestingController);
+});
+
+afterEach(() => httpMock.verify());
+```
+
+## Service Testing
+
+Inject services directly without a component fixture:
+
+```typescript
+describe('UserService', () => {
+ let service: UserService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideHttpClient(), provideHttpClientTesting()],
+ });
+ service = TestBed.inject(UserService);
+ });
+});
+```
+
+## What to Test
+
+- **Services**: All public methods, error paths, HTTP interactions
+- **Components**: Input/output bindings, rendered output for key states, user interactions via harnesses
+- **Pipes**: Pure transformation — plain unit tests, no TestBed needed
+- **Guards/Resolvers**: Return values for allowed and denied states using `RouterTestingHarness`
+
+## E2E Testing
+
+Use the project's configured E2E framework, such as Cypress or Playwright, for critical user flows.
+
+```typescript
+describe('Login flow', () => {
+ it('redirects to dashboard on valid credentials', () => {
+ cy.visit('/login');
+ cy.get('[data-cy=email]').type('user@example.com');
+ cy.get('[data-cy=password]').type('password123');
+ cy.get('[data-cy=submit]').click();
+ cy.url().should('include', '/dashboard');
+ });
+});
+```
+
+- Add `data-cy` attributes to interactive elements for stable selectors
+- Do not rely on CSS classes or text content for selectors in E2E tests
+
+## Coverage
+
+Target ≥80% for services and pipes. Components: test behaviour, not implementation details.
+
+## Skill Reference
+
+See skill: `angular-developer` for comprehensive testing patterns, harness usage, and async best practices.
diff --git a/.claude/rules/ecc/arkts/coding-style.md b/.claude/rules/ecc/arkts/coding-style.md
new file mode 100644
index 0000000..5044ced
--- /dev/null
+++ b/.claude/rules/ecc/arkts/coding-style.md
@@ -0,0 +1,153 @@
+---
+paths:
+ - "**/*.ets"
+ - "**/*.ts"
+ - "**/module.json5"
+ - "**/oh-package.json5"
+ - "**/build-profile.json5"
+---
+# HarmonyOS / ArkTS Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with HarmonyOS and ArkTS-specific content.
+
+## ArkTS Language Constraints
+
+ArkTS is a strict, statically-typed subset of TypeScript. Violating these constraints causes **compilation failures**.
+
+### Type System
+
+- No `any` or `unknown` types - always use explicit types
+- No index access types - use type names directly
+- No conditional type aliases or `infer` keyword
+- No intersection types - use inheritance
+- No mapped types - use classes and regular idioms
+- No `typeof` for type annotations - use explicit type declarations
+- No `as const` assertions - use explicit type annotations
+- No structural typing - use inheritance, interfaces, or type aliases
+- No TypeScript utility types except `Partial`, `Required`, `Readonly`, `Record`
+- For `Record`, index expression type is `V | undefined`
+- Omit type annotations in `catch` clauses (ArkTS does not support `any`/`unknown`)
+
+### Functions & Classes
+
+- No function expressions - use arrow functions
+- No nested functions - use lambdas
+- No generator functions - use `async`/`await` for multitasking
+- No `Function.apply`, `Function.call`, `Function.bind` - follow traditional OOP for `this`
+- No constructor type expressions - use lambdas
+- No constructor signatures in interfaces or object types - use methods or classes
+- No declaring class fields in constructors - declare in class body
+- No `this` in standalone functions or static methods - only in instance methods
+- No `new.target`
+- No definite assignment assertions (`let v!: T`) - use initialized declarations
+- No class literals - introduce named class types
+- No using classes as objects (assigning to variables) - class declarations introduce types, not values
+- Only one static block per class - merge all static statements
+
+### Object & Property Access
+
+- No dynamic field declaration or `obj["field"]` access - use `obj.field` syntax
+- No `delete` operator - use nullable type with `null` to mark absence
+- No prototype assignment - use classes and interfaces
+- No `in` operator - use `instanceof`
+- No reassigning object methods - use wrapper functions or inheritance
+- No `Symbol()` API (except `Symbol.iterator`)
+- No `globalThis` or global scope - use explicit module exports/imports
+- No namespaces as objects - use classes or modules
+- No statements inside namespaces - use functions
+
+### Destructuring & Spread
+
+- No destructuring assignments or variable declarations - use intermediate objects and field-by-field access
+- No destructuring parameter declarations - pass parameters directly, assign local names manually
+- Spread operator only for expanding arrays (or array-derived classes) into rest parameters or array literals
+
+### Modules & Imports
+
+- No `require()` - use regular `import` syntax
+- No `export = ...` - use normal export/import
+- No import assertions - imports are compile-time in ArkTS
+- No UMD modules
+- No wildcards in module names
+- All `import` statements must appear before all other statements
+- TypeScript codebases must not depend on ArkTS codebases via import (reverse is supported)
+
+### Other Restrictions
+
+- No `var` - use `let`
+- No `for...in` loops - use regular `for` loops for arrays
+- No `with` statements
+- No JSX expressions
+- No `#` private identifiers - use `private` keyword
+- No declaration merging (classes, interfaces, enums) - keep definitions compact
+- No index signatures - use arrays
+- Comma operator only in `for` loops
+- Unary operators `+`, `-`, `~` only for numeric types (no implicit string conversion)
+- Enum members: only same-type compile-time expressions for explicit initializers
+- Function return type inference is limited - specify return types explicitly when calling functions with omitted return types
+
+### Object Literals
+
+- Supported only when compiler can infer the corresponding class or interface
+- NOT supported for: `any`/`Object`/`object` types, classes/interfaces with methods, classes with parameterized constructors, classes with `readonly` fields
+
+## Naming Conventions
+
+- Variables / functions: `camelCase` (e.g., `getUserInfo`, `goodsList`)
+- Classes / interfaces: `PascalCase` (e.g., `UserViewModel`, `IGoodsModel`)
+- Constants: `UPPER_SNAKE_CASE` (e.g., `MAX_PAGE_SIZE`, `COLOR_PRIMARY`)
+- File names: `PascalCase` for components (e.g., `HomePage.ets`), `camelCase` for utilities
+
+## Formatting
+
+- Prefer double quotes for strings
+- Semicolons at end of statements
+- Never use `var` - prefer `const`, then `let`
+- All methods, parameters, return values must have complete type annotations
+
+## File Organization
+
+- Component files (`.ets`): one `@ComponentV2` per file
+- ViewModel files: one ViewModel class per file
+- Model files: related data models may share a file
+- Keep files under 400 lines; extract helpers for files approaching 800 lines
+
+## Comments
+
+- File header: `@file` (file purpose) + `@author` (developer), if the project already uses file headers
+- Public methods: JSDoc with `@param`, `@returns`; add `@example` for complex methods
+- Match the project's existing documentation language; use English unless the repository has already standardized on Chinese comments
+
+## Error Handling
+
+```typescript
+// Use try/catch with proper error handling
+try {
+ const result = await riskyOperation()
+ return result
+} catch (error) {
+ hilog.error(0x0000, 'TAG', 'Operation failed: %{public}s', error)
+ throw new Error('User-friendly error message')
+}
+```
+
+## Immutability
+
+Follow the common immutability principles - create new objects instead of mutating:
+
+```typescript
+// BAD: mutation
+function updateUser(user: UserModel, name: string): UserModel {
+ user.name = name // direct mutation
+ return user
+}
+
+// GOOD: immutable - create new instance
+function updateUser(user: UserModel, name: string): UserModel {
+ const updated = new UserModel()
+ updated.id = user.id
+ updated.name = name
+ updated.email = user.email
+ return updated
+}
+```
diff --git a/.claude/rules/ecc/arkts/hooks.md b/.claude/rules/ecc/arkts/hooks.md
new file mode 100644
index 0000000..f870d91
--- /dev/null
+++ b/.claude/rules/ecc/arkts/hooks.md
@@ -0,0 +1,135 @@
+---
+paths:
+ - "**/*.ets"
+ - "**/*.ts"
+ - "**/module.json5"
+ - "**/oh-package.json5"
+---
+# HarmonyOS / ArkTS Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with HarmonyOS-specific build and validation hooks.
+
+## Build Commands
+
+### HAP Package Build
+
+```bash
+# Build HAP package (global hvigor environment)
+hvigorw assembleHap -p product=default
+
+# Build with specific module
+hvigorw assembleHap -p module=entry -p product=default
+
+# Clean build
+hvigorw clean
+```
+
+### DevEco Studio CLI
+
+```bash
+# Check project structure
+hvigorw --version
+
+# Install dependencies
+ohpm install
+
+# Update dependencies
+ohpm update
+```
+
+## Recommended PostToolUse Hooks
+
+### After Editing .ets/.ts Files
+
+Run hvigor build to check for ArkTS compilation errors:
+
+```json
+{
+ "type": "PostToolUse",
+ "matcher": {
+ "tool": ["Edit", "Write"],
+ "filePath": ["**/*.ets", "**/*.ts"]
+ },
+ "hooks": [
+ {
+ "command": "hvigorw assembleHap -p product=default 2>&1 | tail -20",
+ "async": true,
+ "timeout": 60000
+ }
+ ]
+}
+```
+
+### After Editing module.json5
+
+Validate permission and ability declarations:
+
+```json
+{
+ "type": "PostToolUse",
+ "matcher": {
+ "tool": "Edit",
+ "filePath": "**/module.json5"
+ },
+ "hooks": [
+ {
+ "command": "echo '[HarmonyOS] module.json5 modified - verify permissions and abilities'",
+ "async": false
+ }
+ ]
+}
+```
+
+### After Editing oh-package.json5
+
+Reinstall dependencies:
+
+```json
+{
+ "type": "PostToolUse",
+ "matcher": {
+ "tool": "Edit",
+ "filePath": "**/oh-package.json5"
+ },
+ "hooks": [
+ {
+ "command": "ohpm install 2>&1 | tail -10",
+ "async": true,
+ "timeout": 30000
+ }
+ ]
+}
+```
+
+## PreToolUse Hooks
+
+### V1 Decorator Guard
+
+Warn when code contains V1 state management decorators:
+
+```json
+{
+ "type": "PreToolUse",
+ "matcher": {
+ "tool": ["Write", "Edit"],
+ "filePath": "**/*.ets"
+ },
+ "hooks": [
+ {
+ "command": "echo '[HarmonyOS] Reminder: Use @ComponentV2 / @Local / @Param - V1 decorators (@State, @Prop, @Link) are prohibited'"
+ }
+ ]
+}
+```
+
+## Validation Checklist
+
+After each implementation cycle, verify:
+
+- [ ] `hvigorw assembleHap` completes without errors
+- [ ] No V1 decorators in new or modified `.ets` files
+- [ ] No `@ohos.router` imports in new or modified files
+- [ ] All API permissions declared in `module.json5`
+- [ ] All dependencies listed in `oh-package.json5`
+- [ ] Resource strings added to all i18n directories
+- [ ] Dark theme colors provided for new color resources
diff --git a/.claude/rules/ecc/arkts/patterns.md b/.claude/rules/ecc/arkts/patterns.md
new file mode 100644
index 0000000..549a3d1
--- /dev/null
+++ b/.claude/rules/ecc/arkts/patterns.md
@@ -0,0 +1,236 @@
+---
+paths:
+ - "**/*.ets"
+ - "**/*.ts"
+---
+# HarmonyOS / ArkTS Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with HarmonyOS and ArkTS-specific patterns.
+
+## State Management: V2 Only
+
+**MUST use** ArkUI State Management V2. V1 decorators are deprecated and must not be used.
+
+### V2 Decorators
+
+| Decorator | Purpose |
+|-----------|---------|
+| `@ComponentV2` | Marks a struct as a V2 component |
+| `@Local` | Local state within a component |
+| `@Param` | Props received from parent (read-only) |
+| `@Event` | Callback events from child to parent |
+| `@Provider` | Provides state to descendant components |
+| `@Consumer` | Consumes state from ancestor `@Provider` |
+| `@Monitor` | Watches for state changes (replaces V1 `@Watch`) |
+| `@Computed` | Derived/computed values |
+| `@ObservedV2` | Makes a class observable for V2 state management |
+| `@Trace` | Marks observable properties in `@ObservedV2` classes |
+
+### Prohibited V1 Decorators
+
+Never use: `@State`, `@Prop`, `@Link`, `@ObjectLink`, `@Observed`, `@Provide`, `@Consume`, `@Watch`, `@Component` (use `@ComponentV2` instead).
+
+### V2 Component Example
+
+```typescript
+@ObservedV2
+class UserModel {
+ @Trace name: string = ''
+ @Trace age: number = 0
+}
+
+@ComponentV2
+struct UserCard {
+ @Param user: UserModel = new UserModel()
+ @Event onDelete: () => void = () => {}
+
+ build() {
+ Column() {
+ Text(this.user.name)
+ .fontSize($r('app.float.font_size_title'))
+ Text(`${this.user.age}`)
+ .fontSize($r('app.float.font_size_body'))
+ Button($r('app.string.delete'))
+ .onClick(() => this.onDelete())
+ }
+ }
+}
+```
+
+### State Synchronization
+
+```typescript
+@ComponentV2
+struct ParentPage {
+ @Provider('userState') userModel: UserModel = new UserModel()
+
+ build() {
+ Column() {
+ ChildComponent() // automatically receives @Consumer('userState')
+ }
+ }
+}
+
+@ComponentV2
+struct ChildComponent {
+ @Consumer('userState') userModel: UserModel = new UserModel()
+
+ build() {
+ Text(this.userModel.name)
+ }
+}
+```
+
+## Routing: Navigation Only
+
+**MUST use** `Navigation` component with `NavPathStack`. Never use `@ohos.router`.
+
+### Navigation Setup
+
+```typescript
+@ComponentV2
+struct MainPage {
+ @Local navPathStack: NavPathStack = new NavPathStack()
+
+ build() {
+ Navigation(this.navPathStack) {
+ // Home content
+ }
+ .navDestination(this.routerMap)
+ }
+
+ @Builder
+ routerMap(name: string, param: ESObject) {
+ if (name === 'detail') {
+ DetailPage()
+ } else if (name === 'settings') {
+ SettingsPage()
+ }
+ }
+}
+```
+
+### Page Navigation
+
+```typescript
+// Push a new page
+this.navPathStack.pushPath({ name: 'detail', param: { id: '123' } })
+
+// Replace current page
+this.navPathStack.replacePath({ name: 'settings' })
+
+// Pop back
+this.navPathStack.pop()
+
+// Pop to root
+this.navPathStack.clear()
+```
+
+### NavDestination Sub-page
+
+```typescript
+@ComponentV2
+struct DetailPage {
+ build() {
+ NavDestination() {
+ Column() {
+ Text($r('app.string.detail_title'))
+ }
+ }
+ .title($r('app.string.detail_nav_title'))
+ }
+}
+```
+
+## Architecture Pattern: MVVM
+
+Recommended architecture for HarmonyOS applications:
+
+```
+feature/
+ |-- model/ # Data models (@ObservedV2 classes)
+ |-- viewmodel/ # Business logic (ViewModel classes)
+ |-- view/ # UI components (@ComponentV2 structs)
+ |-- service/ # API calls, data access
+```
+
+- **View**: Only rendering logic, no business logic in `build()`
+- **ViewModel**: All business logic encapsulated here
+- **Model**: Pure data classes with `@ObservedV2` and `@Trace`
+- **Service**: Network requests, database operations, file I/O
+
+## ArkUI Animation Patterns
+
+### State-Driven Animation
+
+```typescript
+@ComponentV2
+struct AnimatedCard {
+ @Local isExpanded: boolean = false
+ @Local cardScale: number = 0.8
+
+ build() {
+ Column() {
+ // Content
+ }
+ .scale({ x: this.cardScale, y: this.cardScale })
+ .animation({ duration: 300, curve: Curve.EaseInOut })
+ .onClick(() => {
+ this.isExpanded = !this.isExpanded
+ this.cardScale = this.isExpanded ? 1.0 : 0.8
+ })
+ }
+}
+```
+
+### Animation Rules
+
+- Prefer native HarmonyOS animation APIs and advanced templates
+- Use declarative UI with state-driven animations (change state variables to trigger animations)
+- Set `renderGroup(true)` for complex sub-component animations to reduce render batches
+- **NEVER** frequently change `width`, `height`, `padding`, `margin` during animations - severe performance impact
+- Use `animateTo` for explicit animation control
+- Prefer `transform` (translate, scale, rotate) and `opacity` for performant animations
+
+## Performance Patterns
+
+### LazyForEach for Large Lists
+
+```typescript
+@ComponentV2
+struct LargeList {
+ @Local dataSource: MyDataSource = new MyDataSource()
+
+ build() {
+ List() {
+ LazyForEach(this.dataSource, (item: ItemModel) => {
+ ListItem() {
+ ItemComponent({ item: item })
+ }
+ }, (item: ItemModel) => item.id)
+ }
+ }
+}
+```
+
+### Component Reuse
+
+- Extract reusable components into separate files
+- Use `@Builder` for lightweight UI fragments within a component
+- Use `@Param` for configurable components
+
+## Resource References
+
+Always define UI constants as resources and reference via `$r()`:
+
+```typescript
+// BAD: hardcoded values
+Text('Hello')
+ .fontSize(16)
+ .fontColor('#333333')
+
+// GOOD: resource references
+Text($r('app.string.greeting'))
+ .fontSize($r('app.float.font_size_body'))
+ .fontColor($r('app.color.text_primary'))
+```
diff --git a/.claude/rules/ecc/arkts/security.md b/.claude/rules/ecc/arkts/security.md
new file mode 100644
index 0000000..f767171
--- /dev/null
+++ b/.claude/rules/ecc/arkts/security.md
@@ -0,0 +1,141 @@
+---
+paths:
+ - "**/*.ets"
+ - "**/*.ts"
+ - "**/module.json5"
+---
+# HarmonyOS / ArkTS Security
+
+> This file extends [common/security.md](../common/security.md) with HarmonyOS-specific security practices.
+
+## Permission Management
+
+### Declare Permissions in module.json5
+
+All system API calls requiring permissions must be declared:
+
+```json5
+{
+ "module": {
+ "requestPermissions": [
+ {
+ "name": "ohos.permission.INTERNET",
+ "reason": "$string:internet_permission_reason",
+ "usedScene": {
+ "abilities": ["EntryAbility"],
+ "when": "always"
+ }
+ }
+ ]
+ }
+}
+```
+
+### Permission Checklist
+
+Before calling system APIs, verify:
+
+- [ ] Permission declared in `module.json5`
+- [ ] Permission reason string defined in resources (for user-facing permissions)
+- [ ] Runtime permission request implemented for sensitive permissions (camera, location, etc.)
+- [ ] Permission check before API call with graceful fallback on denial
+
+### Runtime Permission Request
+
+```typescript
+import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
+
+async function checkAndRequestPermission(permission: Permissions): Promise {
+ const atManager = abilityAccessCtrl.createAtManager();
+ const bundleInfo = await bundleManager.getBundleInfoForSelf(
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
+ );
+ const tokenId = bundleInfo.appInfo.accessTokenId;
+ const grantStatus = await atManager.checkAccessToken(tokenId, permission);
+
+ if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
+ return true;
+ }
+
+ const result = await atManager.requestPermissionsFromUser(getContext(), [permission]);
+ return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
+}
+```
+
+## Secret Management
+
+- **NEVER** hardcode API keys, tokens, or passwords in `.ets`/`.ts` source files
+- Use HarmonyOS Preferences API for non-sensitive configuration
+- Use HarmonyOS Keystore for sensitive credentials
+- Environment-specific configs should be managed via build profiles
+
+```typescript
+// BAD: hardcoded secret
+const API_KEY: string = 'sk-xxxxxxxxxxxx';
+
+// GOOD: from build profile config (non-sensitive)
+import { BuildProfile } from 'BuildProfile';
+const endpoint = BuildProfile.API_ENDPOINT;
+
+// GOOD: use HUKS to encrypt/decrypt data without exposing key material
+import { huks } from '@kit.UniversalKeystoreKit';
+async function decryptWithKeystore(alias: string, nonce: Uint8Array, aad: Uint8Array, cipherData: Uint8Array): Promise {
+ const options: huks.HuksOptions = {
+ properties: [
+ { tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES },
+ { tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT },
+ { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM },
+ { tag: huks.HuksTag.HUKS_TAG_PADDING, value: huks.HuksKeyPadding.HUKS_PADDING_NONE },
+ { tag: huks.HuksTag.HUKS_TAG_NONCE, value: nonce },
+ { tag: huks.HuksTag.HUKS_TAG_ASSOCIATED_DATA, value: aad }
+ ],
+ inData: cipherData
+ };
+ const handle = await huks.initSession(alias, options);
+ const result = await huks.finishSession(handle.handle, options);
+ return result.outData;
+}
+```
+
+## Input Validation
+
+- Validate all user input before processing
+- Sanitize data before displaying in UI to prevent injection
+- Validate deep link parameters before navigation
+
+```typescript
+// Validate before navigation
+function handleDeepLink(uri: string): void {
+ const allowedPaths: string[] = ['detail', 'settings', 'profile'];
+ const parsed = new URL(uri);
+ const path = parsed.pathname.replace('/', '');
+
+ if (!allowedPaths.includes(path)) {
+ hilog.warn(0x0000, 'DeepLink', 'Invalid deep link path: %{public}s', path);
+ return;
+ }
+
+ navPathStack.pushPath({ name: path });
+}
+```
+
+## Network Security
+
+- Always use HTTPS for network requests
+- Validate server certificates
+- Implement request timeout and retry policies
+- Never log sensitive data (tokens, user credentials) in network request/response logs
+
+## Data Storage Security
+
+- Use encrypted preferences for sensitive local data
+- Clear sensitive data from memory when no longer needed
+- Implement proper data lifecycle management
+- Consider data classification (public, internal, confidential) when choosing storage mechanisms
+
+## Dependency Security
+
+- Only use dependencies from trusted sources (official ohpm registry)
+- Verify dependency versions in `oh-package.json5`
+- Regularly check for known vulnerabilities in third-party libraries
+- Pin dependency versions to avoid unexpected updates
diff --git a/.claude/rules/ecc/arkts/testing.md b/.claude/rules/ecc/arkts/testing.md
new file mode 100644
index 0000000..a32cfa8
--- /dev/null
+++ b/.claude/rules/ecc/arkts/testing.md
@@ -0,0 +1,126 @@
+---
+paths:
+ - "**/*.ets"
+ - "**/*.ts"
+ - "**/ohosTest/**"
+---
+# HarmonyOS / ArkTS Testing
+
+> This file extends [common/testing.md](../common/testing.md) with HarmonyOS-specific testing practices.
+
+## Test Framework
+
+HarmonyOS uses the built-in test framework with `@ohos.test` capabilities:
+
+- **Unit tests**: Located in `src/ohosTest/ets/test/`
+- **UI tests**: Use `@ohos.UiTest` for component testing
+- **Instrument tests**: Run on device/emulator
+
+## Test Directory Structure
+
+```
+module/
+ |-- src/
+ | |-- main/ets/ # Production code
+ | |-- ohosTest/ets/ # Test code
+ | |-- test/
+ | | |-- Ability.test.ets
+ | | |-- List.test.ets
+ | |-- TestAbility.ets
+ | |-- TestRunner.ets
+```
+
+## Running Tests
+
+```bash
+# Run all tests for a module
+hvigorw testHap -p product=default
+
+# Run tests on connected device
+hdc shell aa test -b com.example.app -m entry_test -s unittest /ets/TestRunner/OpenHarmonyTestRunner
+```
+
+## Unit Test Example
+
+```typescript
+import { describe, it, expect } from '@ohos/hypium';
+
+export default function UserViewModelTest() {
+ describe('UserViewModel', () => {
+ it('should_initialize_with_empty_state', 0, () => {
+ const vm = new UserViewModel();
+ expect(vm.userName).assertEqual('');
+ expect(vm.isLoading).assertFalse();
+ });
+
+ it('should_update_user_name', 0, () => {
+ const vm = new UserViewModel();
+ vm.updateUserName('Alice');
+ expect(vm.userName).assertEqual('Alice');
+ });
+
+ it('should_handle_empty_input', 0, () => {
+ const vm = new UserViewModel();
+ vm.updateUserName('');
+ expect(vm.userName).assertEqual('');
+ expect(vm.hasError).assertFalse();
+ });
+ });
+}
+```
+
+## UI Test Example
+
+```typescript
+import { describe, it, expect } from '@ohos/hypium';
+import { Driver, ON } from '@ohos.UiTest';
+
+export default function HomePageUITest() {
+ describe('HomePage_UI', () => {
+ it('should_display_title', 0, async () => {
+ const driver = Driver.create();
+ await driver.delayMs(1000);
+
+ const title = await driver.findComponent(ON.text('Home'));
+ expect(title !== null).assertTrue();
+ });
+
+ it('should_navigate_to_detail_on_click', 0, async () => {
+ const driver = Driver.create();
+ const button = await driver.findComponent(ON.id('detailButton'));
+ await button.click();
+ await driver.delayMs(500);
+
+ const detailTitle = await driver.findComponent(ON.text('Detail'));
+ expect(detailTitle !== null).assertTrue();
+ });
+ });
+}
+```
+
+## TDD Workflow for HarmonyOS
+
+Follow the standard TDD cycle adapted for HarmonyOS:
+
+1. **RED**: Write a failing test in `ohosTest/ets/test/`
+2. **GREEN**: Implement minimal code in `main/ets/` to pass
+3. **REFACTOR**: Clean up while keeping tests green
+4. **BUILD**: Run `hvigorw assembleHap` to verify compilation
+5. **VERIFY**: Run tests on device/emulator
+
+## Test Coverage Requirements
+
+- Minimum 80% coverage for all critical application code (ViewModels, services, utilities)
+- **Unit tests**: All utility functions, ViewModel logic, data models
+- **Integration tests**: API calls, database operations, cross-module interactions
+- **E2E / UI tests**: Critical user flows (login, navigation, data submission)
+- Test edge cases: empty data, network errors, permission denials
+
+## Testing Best Practices
+
+- Keep tests independent - no shared mutable state between tests
+- Mock network calls and system APIs in unit tests
+- Use meaningful test names: `should_[expected_behavior]_when_[condition]`
+- Test V2 state management reactivity: verify `@Trace` properties trigger UI updates
+- Test Navigation flows: verify `NavPathStack` push/pop/replace operations
+- Avoid testing framework internals - focus on business logic and user-visible behavior
diff --git a/.claude/rules/ecc/common/agents.md b/.claude/rules/ecc/common/agents.md
new file mode 100644
index 0000000..d7dd1be
--- /dev/null
+++ b/.claude/rules/ecc/common/agents.md
@@ -0,0 +1,51 @@
+# Agent Orchestration
+
+## Available Agents
+
+Located in `~/.claude/agents/`:
+
+| Agent | Purpose | When to Use |
+|-------|---------|-------------|
+| planner | Implementation planning | Complex features, refactoring |
+| architect | System design | Architectural decisions |
+| tdd-guide | Test-driven development | New features, bug fixes |
+| code-reviewer | Code review | After writing code |
+| security-reviewer | Security analysis | Before commits |
+| build-error-resolver | Fix build errors | When build fails |
+| e2e-runner | E2E testing | Critical user flows |
+| refactor-cleaner | Dead code cleanup | Code maintenance |
+| doc-updater | Documentation | Updating docs |
+| rust-reviewer | Rust code review | Rust projects |
+| harmonyos-app-resolver | HarmonyOS app development | HarmonyOS/ArkTS projects |
+
+## Immediate Agent Usage
+
+No user prompt needed:
+1. Complex feature requests - Use **planner** agent
+2. Code just written/modified - Use **code-reviewer** agent
+3. Bug fix or new feature - Use **tdd-guide** agent
+4. Architectural decision - Use **architect** agent
+
+## Parallel Task Execution
+
+ALWAYS use parallel Task execution for independent operations:
+
+```markdown
+# GOOD: Parallel execution
+Launch 3 agents in parallel:
+1. Agent 1: Security analysis of auth module
+2. Agent 2: Performance review of cache system
+3. Agent 3: Type checking of utilities
+
+# BAD: Sequential when unnecessary
+First agent 1, then agent 2, then agent 3
+```
+
+## Multi-Perspective Analysis
+
+For complex problems, use split role sub-agents:
+- Factual reviewer
+- Senior engineer
+- Security expert
+- Consistency reviewer
+- Redundancy checker
diff --git a/.claude/rules/ecc/common/code-review.md b/.claude/rules/ecc/common/code-review.md
new file mode 100644
index 0000000..d79ba9b
--- /dev/null
+++ b/.claude/rules/ecc/common/code-review.md
@@ -0,0 +1,124 @@
+# Code Review Standards
+
+## Purpose
+
+Code review ensures quality, security, and maintainability before code is merged. This rule defines when and how to conduct code reviews.
+
+## When to Review
+
+**MANDATORY review triggers:**
+
+- After writing or modifying code
+- Before any commit to shared branches
+- When security-sensitive code is changed (auth, payments, user data)
+- When architectural changes are made
+- Before merging pull requests
+
+**Pre-Review Requirements:**
+
+Before requesting review, ensure:
+
+- All automated checks (CI/CD) are passing
+- Merge conflicts are resolved
+- Branch is up to date with target branch
+
+## Review Checklist
+
+Before marking code complete:
+
+- [ ] Code is readable and well-named
+- [ ] Functions are focused (<50 lines)
+- [ ] Files are cohesive (<800 lines)
+- [ ] No deep nesting (>4 levels)
+- [ ] Errors are handled explicitly
+- [ ] No hardcoded secrets or credentials
+- [ ] No console.log or debug statements
+- [ ] Tests exist for new functionality
+- [ ] Test coverage meets 80% minimum
+
+## Security Review Triggers
+
+**STOP and use security-reviewer agent when:**
+
+- Authentication or authorization code
+- User input handling
+- Database queries
+- File system operations
+- External API calls
+- Cryptographic operations
+- Payment or financial code
+
+## Review Severity Levels
+
+| Level | Meaning | Action |
+|-------|---------|--------|
+| CRITICAL | Security vulnerability or data loss risk | **BLOCK** - Must fix before merge |
+| HIGH | Bug or significant quality issue | **WARN** - Should fix before merge |
+| MEDIUM | Maintainability concern | **INFO** - Consider fixing |
+| LOW | Style or minor suggestion | **NOTE** - Optional |
+
+## Agent Usage
+
+Use these agents for code review:
+
+| Agent | Purpose |
+|-------|---------|
+| **code-reviewer** | General code quality, patterns, best practices |
+| **security-reviewer** | Security vulnerabilities, OWASP Top 10 |
+| **typescript-reviewer** | TypeScript/JavaScript specific issues |
+| **python-reviewer** | Python specific issues |
+| **go-reviewer** | Go specific issues |
+| **rust-reviewer** | Rust specific issues |
+
+## Review Workflow
+
+```
+1. Run git diff to understand changes
+2. Check security checklist first
+3. Review code quality checklist
+4. Run relevant tests
+5. Verify coverage >= 80%
+6. Use appropriate agent for detailed review
+```
+
+## Common Issues to Catch
+
+### Security
+
+- Hardcoded credentials (API keys, passwords, tokens)
+- SQL injection (string concatenation in queries)
+- XSS vulnerabilities (unescaped user input)
+- Path traversal (unsanitized file paths)
+- CSRF protection missing
+- Authentication bypasses
+
+### Code Quality
+
+- Large functions (>50 lines) - split into smaller
+- Large files (>800 lines) - extract modules
+- Deep nesting (>4 levels) - use early returns
+- Missing error handling - handle explicitly
+- Mutation patterns - prefer immutable operations
+- Missing tests - add test coverage
+
+### Performance
+
+- N+1 queries - use JOINs or batching
+- Missing pagination - add LIMIT to queries
+- Unbounded queries - add constraints
+- Missing caching - cache expensive operations
+
+## Approval Criteria
+
+- **Approve**: No CRITICAL or HIGH issues
+- **Warning**: Only HIGH issues (merge with caution)
+- **Block**: CRITICAL issues found
+
+## Integration with Other Rules
+
+This rule works with:
+
+- [testing.md](testing.md) - Test coverage requirements
+- [security.md](security.md) - Security checklist
+- [git-workflow.md](git-workflow.md) - Commit standards
+- [agents.md](agents.md) - Agent delegation
diff --git a/.claude/rules/ecc/common/coding-style.md b/.claude/rules/ecc/common/coding-style.md
new file mode 100644
index 0000000..e72f3f1
--- /dev/null
+++ b/.claude/rules/ecc/common/coding-style.md
@@ -0,0 +1,90 @@
+# Coding Style
+
+## Immutability (CRITICAL)
+
+ALWAYS create new objects, NEVER mutate existing ones:
+
+```
+// Pseudocode
+WRONG: modify(original, field, value) → changes original in-place
+CORRECT: update(original, field, value) → returns new copy with change
+```
+
+Rationale: Immutable data prevents hidden side effects, makes debugging easier, and enables safe concurrency.
+
+## Core Principles
+
+### KISS (Keep It Simple)
+
+- Prefer the simplest solution that actually works
+- Avoid premature optimization
+- Optimize for clarity over cleverness
+
+### DRY (Don't Repeat Yourself)
+
+- Extract repeated logic into shared functions or utilities
+- Avoid copy-paste implementation drift
+- Introduce abstractions when repetition is real, not speculative
+
+### YAGNI (You Aren't Gonna Need It)
+
+- Do not build features or abstractions before they are needed
+- Avoid speculative generality
+- Start simple, then refactor when the pressure is real
+
+## File Organization
+
+MANY SMALL FILES > FEW LARGE FILES:
+- High cohesion, low coupling
+- 200-400 lines typical, 800 max
+- Extract utilities from large modules
+- Organize by feature/domain, not by type
+
+## Error Handling
+
+ALWAYS handle errors comprehensively:
+- Handle errors explicitly at every level
+- Provide user-friendly error messages in UI-facing code
+- Log detailed error context on the server side
+- Never silently swallow errors
+
+## Input Validation
+
+ALWAYS validate at system boundaries:
+- Validate all user input before processing
+- Use schema-based validation where available
+- Fail fast with clear error messages
+- Never trust external data (API responses, user input, file content)
+
+## Naming Conventions
+
+- Variables and functions: `camelCase` with descriptive names
+- Booleans: prefer `is`, `has`, `should`, or `can` prefixes
+- Interfaces, types, and components: `PascalCase`
+- Constants: `UPPER_SNAKE_CASE`
+- Custom hooks: `camelCase` with a `use` prefix
+
+## Code Smells to Avoid
+
+### Deep Nesting
+
+Prefer early returns over nested conditionals once the logic starts stacking.
+
+### Magic Numbers
+
+Use named constants for meaningful thresholds, delays, and limits.
+
+### Long Functions
+
+Split large functions into focused pieces with clear responsibilities.
+
+## Code Quality Checklist
+
+Before marking work complete:
+- [ ] Code is readable and well-named
+- [ ] Functions are small (<50 lines)
+- [ ] Files are focused (<800 lines)
+- [ ] No deep nesting (>4 levels)
+- [ ] Proper error handling
+- [ ] No hardcoded values (use constants or config)
+- [ ] No mutation (immutable patterns used)
diff --git a/.claude/rules/ecc/common/development-workflow.md b/.claude/rules/ecc/common/development-workflow.md
new file mode 100644
index 0000000..ae070be
--- /dev/null
+++ b/.claude/rules/ecc/common/development-workflow.md
@@ -0,0 +1,44 @@
+# Development Workflow
+
+> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations.
+
+The Feature Implementation Workflow describes the development pipeline: research, planning, TDD, code review, and then committing to git.
+
+## Feature Implementation Workflow
+
+0. **Research & Reuse** _(mandatory before any new implementation)_
+ - **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new.
+ - **Library docs second:** Use Context7 or primary vendor docs to confirm API behavior, package usage, and version-specific details before implementing.
+ - **Exa only when the first two are insufficient:** Use Exa for broader web research or discovery after GitHub search and primary docs.
+ - **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions.
+ - **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped.
+ - Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement.
+
+1. **Plan First**
+ - Use **planner** agent to create implementation plan
+ - Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list
+ - Identify dependencies and risks
+ - Break down into phases
+
+2. **TDD Approach**
+ - Use **tdd-guide** agent
+ - Write tests first (RED)
+ - Implement to pass tests (GREEN)
+ - Refactor (IMPROVE)
+ - Verify 80%+ coverage
+
+3. **Code Review**
+ - Use **code-reviewer** agent immediately after writing code
+ - Address CRITICAL and HIGH issues
+ - Fix MEDIUM issues when possible
+
+4. **Commit & Push**
+ - Detailed commit messages
+ - Follow conventional commits format
+ - See [git-workflow.md](./git-workflow.md) for commit message format and PR process
+
+5. **Pre-Review Checks**
+ - Verify all automated checks (CI/CD) are passing
+ - Resolve any merge conflicts
+ - Ensure branch is up to date with target branch
+ - Only request review after these checks pass
diff --git a/.claude/rules/ecc/common/git-workflow.md b/.claude/rules/ecc/common/git-workflow.md
new file mode 100644
index 0000000..d57d9e2
--- /dev/null
+++ b/.claude/rules/ecc/common/git-workflow.md
@@ -0,0 +1,24 @@
+# Git Workflow
+
+## Commit Message Format
+```
+:
+
+
+```
+
+Types: feat, fix, refactor, docs, test, chore, perf, ci
+
+Note: Attribution disabled globally via ~/.claude/settings.json.
+
+## Pull Request Workflow
+
+When creating PRs:
+1. Analyze full commit history (not just latest commit)
+2. Use `git diff [base-branch]...HEAD` to see all changes
+3. Draft comprehensive PR summary
+4. Include test plan with TODOs
+5. Push with `-u` flag if new branch
+
+> For the full development process (planning, TDD, code review) before git operations,
+> see [development-workflow.md](./development-workflow.md).
diff --git a/.claude/rules/ecc/common/hooks.md b/.claude/rules/ecc/common/hooks.md
new file mode 100644
index 0000000..5439408
--- /dev/null
+++ b/.claude/rules/ecc/common/hooks.md
@@ -0,0 +1,30 @@
+# Hooks System
+
+## Hook Types
+
+- **PreToolUse**: Before tool execution (validation, parameter modification)
+- **PostToolUse**: After tool execution (auto-format, checks)
+- **Stop**: When session ends (final verification)
+
+## Auto-Accept Permissions
+
+Use with caution:
+- Enable for trusted, well-defined plans
+- Disable for exploratory work
+- Never use dangerously-skip-permissions flag
+- Configure `allowedTools` in `~/.claude.json` instead
+
+## TodoWrite Best Practices
+
+Use TodoWrite tool to:
+- Track progress on multi-step tasks
+- Verify understanding of instructions
+- Enable real-time steering
+- Show granular implementation steps
+
+Todo list reveals:
+- Out of order steps
+- Missing items
+- Extra unnecessary items
+- Wrong granularity
+- Misinterpreted requirements
diff --git a/.claude/rules/ecc/common/patterns.md b/.claude/rules/ecc/common/patterns.md
new file mode 100644
index 0000000..959939f
--- /dev/null
+++ b/.claude/rules/ecc/common/patterns.md
@@ -0,0 +1,31 @@
+# Common Patterns
+
+## Skeleton Projects
+
+When implementing new functionality:
+1. Search for battle-tested skeleton projects
+2. Use parallel agents to evaluate options:
+ - Security assessment
+ - Extensibility analysis
+ - Relevance scoring
+ - Implementation planning
+3. Clone best match as foundation
+4. Iterate within proven structure
+
+## Design Patterns
+
+### Repository Pattern
+
+Encapsulate data access behind a consistent interface:
+- Define standard operations: findAll, findById, create, update, delete
+- Concrete implementations handle storage details (database, API, file, etc.)
+- Business logic depends on the abstract interface, not the storage mechanism
+- Enables easy swapping of data sources and simplifies testing with mocks
+
+### API Response Format
+
+Use a consistent envelope for all API responses:
+- Include a success/status indicator
+- Include the data payload (nullable on error)
+- Include an error message field (nullable on success)
+- Include metadata for paginated responses (total, page, limit)
diff --git a/.claude/rules/ecc/common/performance.md b/.claude/rules/ecc/common/performance.md
new file mode 100644
index 0000000..0a2f229
--- /dev/null
+++ b/.claude/rules/ecc/common/performance.md
@@ -0,0 +1,55 @@
+# Performance Optimization
+
+## Model Selection Strategy
+
+**Haiku** (90% of Sonnet capability, 3x cost savings):
+- Lightweight agents with frequent invocation
+- Pair programming and code generation
+- Worker agents in multi-agent systems
+
+**Sonnet** (Best coding model):
+- Main development work
+- Orchestrating multi-agent workflows
+- Complex coding tasks
+
+**Opus** (Deepest reasoning):
+- Complex architectural decisions
+- Maximum reasoning requirements
+- Research and analysis tasks
+
+## Context Window Management
+
+Avoid last 20% of context window for:
+- Large-scale refactoring
+- Feature implementation spanning multiple files
+- Debugging complex interactions
+
+Lower context sensitivity tasks:
+- Single-file edits
+- Independent utility creation
+- Documentation updates
+- Simple bug fixes
+
+## Extended Thinking + Plan Mode
+
+Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning.
+
+Control extended thinking via:
+- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux)
+- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json`
+- **Budget cap**: `export MAX_THINKING_TOKENS=10000` (bash) or `$env:MAX_THINKING_TOKENS = "10000"` (PowerShell)
+- **Verbose mode**: Ctrl+O to see thinking output
+
+For complex tasks requiring deep reasoning:
+1. Ensure extended thinking is enabled (on by default)
+2. Enable **Plan Mode** for structured approach
+3. Use multiple critique rounds for thorough analysis
+4. Use split role sub-agents for diverse perspectives
+
+## Build Troubleshooting
+
+If build fails:
+1. Use **build-error-resolver** agent
+2. Analyze error messages
+3. Fix incrementally
+4. Verify after each fix
diff --git a/.claude/rules/ecc/common/security.md b/.claude/rules/ecc/common/security.md
new file mode 100644
index 0000000..49624c0
--- /dev/null
+++ b/.claude/rules/ecc/common/security.md
@@ -0,0 +1,29 @@
+# Security Guidelines
+
+## Mandatory Security Checks
+
+Before ANY commit:
+- [ ] No hardcoded secrets (API keys, passwords, tokens)
+- [ ] All user inputs validated
+- [ ] SQL injection prevention (parameterized queries)
+- [ ] XSS prevention (sanitized HTML)
+- [ ] CSRF protection enabled
+- [ ] Authentication/authorization verified
+- [ ] Rate limiting on all endpoints
+- [ ] Error messages don't leak sensitive data
+
+## Secret Management
+
+- NEVER hardcode secrets in source code
+- ALWAYS use environment variables or a secret manager
+- Validate that required secrets are present at startup
+- Rotate any secrets that may have been exposed
+
+## Security Response Protocol
+
+If security issue found:
+1. STOP immediately
+2. Use **security-reviewer** agent
+3. Fix CRITICAL issues before continuing
+4. Rotate any exposed secrets
+5. Review entire codebase for similar issues
diff --git a/.claude/rules/ecc/common/testing.md b/.claude/rules/ecc/common/testing.md
new file mode 100644
index 0000000..416c1c2
--- /dev/null
+++ b/.claude/rules/ecc/common/testing.md
@@ -0,0 +1,57 @@
+# Testing Requirements
+
+## Minimum Test Coverage: 80%
+
+Test Types (ALL required):
+1. **Unit Tests** - Individual functions, utilities, components
+2. **Integration Tests** - API endpoints, database operations
+3. **E2E Tests** - Critical user flows (framework chosen per language)
+
+## Test-Driven Development
+
+MANDATORY workflow:
+1. Write test first (RED)
+2. Run test - it should FAIL
+3. Write minimal implementation (GREEN)
+4. Run test - it should PASS
+5. Refactor (IMPROVE)
+6. Verify coverage (80%+)
+
+## Troubleshooting Test Failures
+
+1. Use **tdd-guide** agent
+2. Check test isolation
+3. Verify mocks are correct
+4. Fix implementation, not tests (unless tests are wrong)
+
+## Agent Support
+
+- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first
+
+## Test Structure (AAA Pattern)
+
+Prefer Arrange-Act-Assert structure for tests:
+
+```typescript
+test('calculates similarity correctly', () => {
+ // Arrange
+ const vector1 = [1, 0, 0]
+ const vector2 = [0, 1, 0]
+
+ // Act
+ const similarity = calculateCosineSimilarity(vector1, vector2)
+
+ // Assert
+ expect(similarity).toBe(0)
+})
+```
+
+### Test Naming
+
+Use descriptive names that explain the behavior under test:
+
+```typescript
+test('returns empty array when no markets match query', () => {})
+test('throws error when API key is missing', () => {})
+test('falls back to substring search when Redis is unavailable', () => {})
+```
diff --git a/.claude/rules/ecc/cpp/coding-style.md b/.claude/rules/ecc/cpp/coding-style.md
new file mode 100644
index 0000000..3550077
--- /dev/null
+++ b/.claude/rules/ecc/cpp/coding-style.md
@@ -0,0 +1,44 @@
+---
+paths:
+ - "**/*.cpp"
+ - "**/*.hpp"
+ - "**/*.cc"
+ - "**/*.hh"
+ - "**/*.cxx"
+ - "**/*.h"
+ - "**/CMakeLists.txt"
+---
+# C++ Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with C++ specific content.
+
+## Modern C++ (C++17/20/23)
+
+- Prefer **modern C++ features** over C-style constructs
+- Use `auto` when the type is obvious from context
+- Use `constexpr` for compile-time constants
+- Use structured bindings: `auto [key, value] = map_entry;`
+
+## Resource Management
+
+- **RAII everywhere** — no manual `new`/`delete`
+- Use `std::unique_ptr` for exclusive ownership
+- Use `std::shared_ptr` only when shared ownership is truly needed
+- Use `std::make_unique` / `std::make_shared` over raw `new`
+
+## Naming Conventions
+
+- Types/Classes: `PascalCase`
+- Functions/Methods: `snake_case` or `camelCase` (follow project convention)
+- Constants: `kPascalCase` or `UPPER_SNAKE_CASE`
+- Namespaces: `lowercase`
+- Member variables: `snake_case_` (trailing underscore) or `m_` prefix
+
+## Formatting
+
+- Use **clang-format** — no style debates
+- Run `clang-format -i ` before committing
+
+## Reference
+
+See skill: `cpp-coding-standards` for comprehensive C++ coding standards and guidelines.
diff --git a/.claude/rules/ecc/cpp/hooks.md b/.claude/rules/ecc/cpp/hooks.md
new file mode 100644
index 0000000..4ab677a
--- /dev/null
+++ b/.claude/rules/ecc/cpp/hooks.md
@@ -0,0 +1,39 @@
+---
+paths:
+ - "**/*.cpp"
+ - "**/*.hpp"
+ - "**/*.cc"
+ - "**/*.hh"
+ - "**/*.cxx"
+ - "**/*.h"
+ - "**/CMakeLists.txt"
+---
+# C++ Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with C++ specific content.
+
+## Build Hooks
+
+Run these checks before committing C++ changes:
+
+```bash
+# Format check
+clang-format --dry-run --Werror src/*.cpp src/*.hpp
+
+# Static analysis
+clang-tidy src/*.cpp -- -std=c++17
+
+# Build
+cmake --build build
+
+# Tests
+ctest --test-dir build --output-on-failure
+```
+
+## Recommended CI Pipeline
+
+1. **clang-format** — formatting check
+2. **clang-tidy** — static analysis
+3. **cppcheck** — additional analysis
+4. **cmake build** — compilation
+5. **ctest** — test execution with sanitizers
diff --git a/.claude/rules/ecc/cpp/patterns.md b/.claude/rules/ecc/cpp/patterns.md
new file mode 100644
index 0000000..0c156e8
--- /dev/null
+++ b/.claude/rules/ecc/cpp/patterns.md
@@ -0,0 +1,51 @@
+---
+paths:
+ - "**/*.cpp"
+ - "**/*.hpp"
+ - "**/*.cc"
+ - "**/*.hh"
+ - "**/*.cxx"
+ - "**/*.h"
+ - "**/CMakeLists.txt"
+---
+# C++ Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with C++ specific content.
+
+## RAII (Resource Acquisition Is Initialization)
+
+Tie resource lifetime to object lifetime:
+
+```cpp
+class FileHandle {
+public:
+ explicit FileHandle(const std::string& path) : file_(std::fopen(path.c_str(), "r")) {}
+ ~FileHandle() { if (file_) std::fclose(file_); }
+ FileHandle(const FileHandle&) = delete;
+ FileHandle& operator=(const FileHandle&) = delete;
+private:
+ std::FILE* file_;
+};
+```
+
+## Rule of Five/Zero
+
+- **Rule of Zero**: Prefer classes that need no custom destructor, copy/move constructors, or assignments
+- **Rule of Five**: If you define any of destructor/copy-ctor/copy-assign/move-ctor/move-assign, define all five
+
+## Value Semantics
+
+- Pass small/trivial types by value
+- Pass large types by `const&`
+- Return by value (rely on RVO/NRVO)
+- Use move semantics for sink parameters
+
+## Error Handling
+
+- Use exceptions for exceptional conditions
+- Use `std::optional` for values that may not exist
+- Use `std::expected` (C++23) or result types for expected failures
+
+## Reference
+
+See skill: `cpp-coding-standards` for comprehensive C++ patterns and anti-patterns.
diff --git a/.claude/rules/ecc/cpp/security.md b/.claude/rules/ecc/cpp/security.md
new file mode 100644
index 0000000..0ee9f5f
--- /dev/null
+++ b/.claude/rules/ecc/cpp/security.md
@@ -0,0 +1,51 @@
+---
+paths:
+ - "**/*.cpp"
+ - "**/*.hpp"
+ - "**/*.cc"
+ - "**/*.hh"
+ - "**/*.cxx"
+ - "**/*.h"
+ - "**/CMakeLists.txt"
+---
+# C++ Security
+
+> This file extends [common/security.md](../common/security.md) with C++ specific content.
+
+## Memory Safety
+
+- Never use raw `new`/`delete` — use smart pointers
+- Never use C-style arrays — use `std::array` or `std::vector`
+- Never use `malloc`/`free` — use C++ allocation
+- Avoid `reinterpret_cast` unless absolutely necessary
+
+## Buffer Overflows
+
+- Use `std::string` over `char*`
+- Use `.at()` for bounds-checked access when safety matters
+- Never use `strcpy`, `strcat`, `sprintf` — use `std::string` or `fmt::format`
+
+## Undefined Behavior
+
+- Always initialize variables
+- Avoid signed integer overflow
+- Never dereference null or dangling pointers
+- Use sanitizers in CI:
+ ```bash
+ cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
+ ```
+
+## Static Analysis
+
+- Use **clang-tidy** for automated checks:
+ ```bash
+ clang-tidy --checks='*' src/*.cpp
+ ```
+- Use **cppcheck** for additional analysis:
+ ```bash
+ cppcheck --enable=all src/
+ ```
+
+## Reference
+
+See skill: `cpp-coding-standards` for detailed security guidelines.
diff --git a/.claude/rules/ecc/cpp/testing.md b/.claude/rules/ecc/cpp/testing.md
new file mode 100644
index 0000000..7c28355
--- /dev/null
+++ b/.claude/rules/ecc/cpp/testing.md
@@ -0,0 +1,44 @@
+---
+paths:
+ - "**/*.cpp"
+ - "**/*.hpp"
+ - "**/*.cc"
+ - "**/*.hh"
+ - "**/*.cxx"
+ - "**/*.h"
+ - "**/CMakeLists.txt"
+---
+# C++ Testing
+
+> This file extends [common/testing.md](../common/testing.md) with C++ specific content.
+
+## Framework
+
+Use **GoogleTest** (gtest/gmock) with **CMake/CTest**.
+
+## Running Tests
+
+```bash
+cmake --build build && ctest --test-dir build --output-on-failure
+```
+
+## Coverage
+
+```bash
+cmake -DCMAKE_CXX_FLAGS="--coverage" -DCMAKE_EXE_LINKER_FLAGS="--coverage" ..
+cmake --build .
+ctest --output-on-failure
+lcov --capture --directory . --output-file coverage.info
+```
+
+## Sanitizers
+
+Always run tests with sanitizers in CI:
+
+```bash
+cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" ..
+```
+
+## Reference
+
+See skill: `cpp-testing` for detailed C++ testing patterns, TDD workflow, and GoogleTest/GMock usage.
diff --git a/.claude/rules/ecc/csharp/coding-style.md b/.claude/rules/ecc/csharp/coding-style.md
new file mode 100644
index 0000000..d97aaad
--- /dev/null
+++ b/.claude/rules/ecc/csharp/coding-style.md
@@ -0,0 +1,72 @@
+---
+paths:
+ - "**/*.cs"
+ - "**/*.csx"
+---
+# C# Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with C#-specific content.
+
+## Standards
+
+- Follow current .NET conventions and enable nullable reference types
+- Prefer explicit access modifiers on public and internal APIs
+- Keep files aligned with the primary type they define
+
+## Types and Models
+
+- Prefer `record` or `record struct` for immutable value-like models
+- Use `class` for entities or types with identity and lifecycle
+- Use `interface` for service boundaries and abstractions
+- Avoid `dynamic` in application code; prefer generics or explicit models
+
+```csharp
+public sealed record UserDto(Guid Id, string Email);
+
+public interface IUserRepository
+{
+ Task FindByIdAsync(Guid id, CancellationToken cancellationToken);
+}
+```
+
+## Immutability
+
+- Prefer `init` setters, constructor parameters, and immutable collections for shared state
+- Do not mutate input models in-place when producing updated state
+
+```csharp
+public sealed record UserProfile(string Name, string Email);
+
+public static UserProfile Rename(UserProfile profile, string name) =>
+ profile with { Name = name };
+```
+
+## Async and Error Handling
+
+- Prefer `async`/`await` over blocking calls like `.Result` or `.Wait()`
+- Pass `CancellationToken` through public async APIs
+- Throw specific exceptions and log with structured properties
+
+```csharp
+public async Task LoadOrderAsync(
+ Guid orderId,
+ CancellationToken cancellationToken)
+{
+ try
+ {
+ return await repository.FindAsync(orderId, cancellationToken)
+ ?? throw new InvalidOperationException($"Order {orderId} was not found.");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to load order {OrderId}", orderId);
+ throw;
+ }
+}
+```
+
+## Formatting
+
+- Use `dotnet format` for formatting and analyzer fixes
+- Keep `using` directives organized and remove unused imports
+- Prefer expression-bodied members only when they stay readable
diff --git a/.claude/rules/ecc/csharp/hooks.md b/.claude/rules/ecc/csharp/hooks.md
new file mode 100644
index 0000000..f7a46ef
--- /dev/null
+++ b/.claude/rules/ecc/csharp/hooks.md
@@ -0,0 +1,25 @@
+---
+paths:
+ - "**/*.cs"
+ - "**/*.csx"
+ - "**/*.csproj"
+ - "**/*.sln"
+ - "**/Directory.Build.props"
+ - "**/Directory.Build.targets"
+---
+# C# Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with C#-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **dotnet format**: Auto-format edited C# files and apply analyzer fixes
+- **dotnet build**: Verify the solution or project still compiles after edits
+- **dotnet test --no-build**: Re-run the nearest relevant test project after behavior changes
+
+## Stop Hooks
+
+- Run a final `dotnet build` before ending a session with broad C# changes
+- Warn on modified `appsettings*.json` files so secrets do not get committed
diff --git a/.claude/rules/ecc/csharp/patterns.md b/.claude/rules/ecc/csharp/patterns.md
new file mode 100644
index 0000000..a94aba7
--- /dev/null
+++ b/.claude/rules/ecc/csharp/patterns.md
@@ -0,0 +1,50 @@
+---
+paths:
+ - "**/*.cs"
+ - "**/*.csx"
+---
+# C# Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with C#-specific content.
+
+## API Response Pattern
+
+```csharp
+public sealed record ApiResponse(
+ bool Success,
+ T? Data = default,
+ string? Error = null,
+ object? Meta = null);
+```
+
+## Repository Pattern
+
+```csharp
+public interface IRepository
+{
+ Task> FindAllAsync(CancellationToken cancellationToken);
+ Task FindByIdAsync(Guid id, CancellationToken cancellationToken);
+ Task CreateAsync(T entity, CancellationToken cancellationToken);
+ Task UpdateAsync(T entity, CancellationToken cancellationToken);
+ Task DeleteAsync(Guid id, CancellationToken cancellationToken);
+}
+```
+
+## Options Pattern
+
+Use strongly typed options for config instead of reading raw strings throughout the codebase.
+
+```csharp
+public sealed class PaymentsOptions
+{
+ public const string SectionName = "Payments";
+ public required string BaseUrl { get; init; }
+ public required string ApiKeySecretName { get; init; }
+}
+```
+
+## Dependency Injection
+
+- Depend on interfaces at service boundaries
+- Keep constructors focused; if a service needs too many dependencies, split responsibilities
+- Register lifetimes intentionally: singleton for stateless/shared services, scoped for request data, transient for lightweight pure workers
diff --git a/.claude/rules/ecc/csharp/security.md b/.claude/rules/ecc/csharp/security.md
new file mode 100644
index 0000000..9eb7c23
--- /dev/null
+++ b/.claude/rules/ecc/csharp/security.md
@@ -0,0 +1,58 @@
+---
+paths:
+ - "**/*.cs"
+ - "**/*.csx"
+ - "**/*.csproj"
+ - "**/appsettings*.json"
+---
+# C# Security
+
+> This file extends [common/security.md](../common/security.md) with C#-specific content.
+
+## Secret Management
+
+- Never hardcode API keys, tokens, or connection strings in source code
+- Use environment variables, user secrets for local development, and a secret manager in production
+- Keep `appsettings.*.json` free of real credentials
+
+```csharp
+// BAD
+const string ApiKey = "sk-live-123";
+
+// GOOD
+var apiKey = builder.Configuration["OpenAI:ApiKey"]
+ ?? throw new InvalidOperationException("OpenAI:ApiKey is not configured.");
+```
+
+## SQL Injection Prevention
+
+- Always use parameterized queries with ADO.NET, Dapper, or EF Core
+- Never concatenate user input into SQL strings
+- Validate sort fields and filter operators before using dynamic query composition
+
+```csharp
+const string sql = "SELECT * FROM Orders WHERE CustomerId = @customerId";
+await connection.QueryAsync(sql, new { customerId });
+```
+
+## Input Validation
+
+- Validate DTOs at the application boundary
+- Use data annotations, FluentValidation, or explicit guard clauses
+- Reject invalid model state before running business logic
+
+## Authentication and Authorization
+
+- Prefer framework auth handlers instead of custom token parsing
+- Enforce authorization policies at endpoint or handler boundaries
+- Never log raw tokens, passwords, or PII
+
+## Error Handling
+
+- Return safe client-facing messages
+- Log detailed exceptions with structured context server-side
+- Do not expose stack traces, SQL text, or filesystem paths in API responses
+
+## References
+
+See skill: `security-review` for broader application security review checklists.
diff --git a/.claude/rules/ecc/csharp/testing.md b/.claude/rules/ecc/csharp/testing.md
new file mode 100644
index 0000000..a00f012
--- /dev/null
+++ b/.claude/rules/ecc/csharp/testing.md
@@ -0,0 +1,46 @@
+---
+paths:
+ - "**/*.cs"
+ - "**/*.csx"
+ - "**/*.csproj"
+---
+# C# Testing
+
+> This file extends [common/testing.md](../common/testing.md) with C#-specific content.
+
+## Test Framework
+
+- Prefer **xUnit** for unit and integration tests
+- Use **FluentAssertions** for readable assertions
+- Use **Moq** or **NSubstitute** for mocking dependencies
+- Use **Testcontainers** when integration tests need real infrastructure
+
+## Test Organization
+
+- Mirror `src/` structure under `tests/`
+- Separate unit, integration, and end-to-end coverage clearly
+- Name tests by behavior, not implementation details
+
+```csharp
+public sealed class OrderServiceTests
+{
+ [Fact]
+ public async Task FindByIdAsync_ReturnsOrder_WhenOrderExists()
+ {
+ // Arrange
+ // Act
+ // Assert
+ }
+}
+```
+
+## ASP.NET Core Integration Tests
+
+- Use `WebApplicationFactory` for API integration coverage
+- Test auth, validation, and serialization through HTTP, not by bypassing middleware
+
+## Coverage
+
+- Target 80%+ line coverage
+- Focus coverage on domain logic, validation, auth, and failure paths
+- Run `dotnet test` in CI with coverage collection enabled where available
diff --git a/.claude/rules/ecc/dart/coding-style.md b/.claude/rules/ecc/dart/coding-style.md
new file mode 100644
index 0000000..f79c1fc
--- /dev/null
+++ b/.claude/rules/ecc/dart/coding-style.md
@@ -0,0 +1,159 @@
+---
+paths:
+ - "**/*.dart"
+ - "**/pubspec.yaml"
+ - "**/analysis_options.yaml"
+---
+# Dart/Flutter Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Dart and Flutter-specific content.
+
+## Formatting
+
+- **dart format** for all `.dart` files — enforced in CI (`dart format --set-exit-if-changed .`)
+- Line length: 80 characters (dart format default)
+- Trailing commas on multi-line argument/parameter lists to improve diffs and formatting
+
+## Immutability
+
+- Prefer `final` for local variables and `const` for compile-time constants
+- Use `const` constructors wherever all fields are `final`
+- Return unmodifiable collections from public APIs (`List.unmodifiable`, `Map.unmodifiable`)
+- Use `copyWith()` for state mutations in immutable state classes
+
+```dart
+// BAD
+var count = 0;
+List items = ['a', 'b'];
+
+// GOOD
+final count = 0;
+const items = ['a', 'b'];
+```
+
+## Naming
+
+Follow Dart conventions:
+- `camelCase` for variables, parameters, and named constructors
+- `PascalCase` for classes, enums, typedefs, and extensions
+- `snake_case` for file names and library names
+- `SCREAMING_SNAKE_CASE` for constants declared with `const` at top level
+- Prefix private members with `_`
+- Extension names describe the type they extend: `StringExtensions`, not `MyHelpers`
+
+## Null Safety
+
+- Avoid `!` (bang operator) — prefer `?.`, `??`, `if (x != null)`, or Dart 3 pattern matching; reserve `!` only where a null value is a programming error and crashing is the right behaviour
+- Avoid `late` unless initialization is guaranteed before first use (prefer nullable or constructor init)
+- Use `required` for constructor parameters that must always be provided
+
+```dart
+// BAD — crashes at runtime if user is null
+final name = user!.name;
+
+// GOOD — null-aware operators
+final name = user?.name ?? 'Unknown';
+
+// GOOD — Dart 3 pattern matching (exhaustive, compiler-checked)
+final name = switch (user) {
+ User(:final name) => name,
+ null => 'Unknown',
+};
+
+// GOOD — early-return null guard
+String getUserName(User? user) {
+ if (user == null) return 'Unknown';
+ return user.name; // promoted to non-null after the guard
+}
+```
+
+## Sealed Types and Pattern Matching (Dart 3+)
+
+Use sealed classes to model closed state hierarchies:
+
+```dart
+sealed class AsyncState {
+ const AsyncState();
+}
+
+final class Loading extends AsyncState {
+ const Loading();
+}
+
+final class Success extends AsyncState {
+ const Success(this.data);
+ final T data;
+}
+
+final class Failure extends AsyncState {
+ const Failure(this.error);
+ final Object error;
+}
+```
+
+Always use exhaustive `switch` with sealed types — no default/wildcard:
+
+```dart
+// BAD
+if (state is Loading) { ... }
+
+// GOOD
+return switch (state) {
+ Loading() => const CircularProgressIndicator(),
+ Success(:final data) => DataWidget(data),
+ Failure(:final error) => ErrorWidget(error.toString()),
+};
+```
+
+## Error Handling
+
+- Specify exception types in `on` clauses — never use bare `catch (e)`
+- Never catch `Error` subtypes — they indicate programming bugs
+- Use `Result`-style types or sealed classes for recoverable errors
+- Avoid using exceptions for control flow
+
+```dart
+// BAD
+try {
+ await fetchUser();
+} catch (e) {
+ log(e.toString());
+}
+
+// GOOD
+try {
+ await fetchUser();
+} on NetworkException catch (e) {
+ log('Network error: ${e.message}');
+} on NotFoundException {
+ handleNotFound();
+}
+```
+
+## Async / Futures
+
+- Always `await` Futures or explicitly call `unawaited()` to signal intentional fire-and-forget
+- Never mark a function `async` if it never `await`s anything
+- Use `Future.wait` / `Future.any` for concurrent operations
+- Check `context.mounted` before using `BuildContext` after any `await` (Flutter 3.7+)
+
+```dart
+// BAD — ignoring Future
+fetchData(); // fire-and-forget without marking intent
+
+// GOOD
+unawaited(fetchData()); // explicit fire-and-forget
+await fetchData(); // or properly awaited
+```
+
+## Imports
+
+- Use `package:` imports throughout — never relative imports (`../`) for cross-feature or cross-layer code
+- Order: `dart:` → external `package:` → internal `package:` (same package)
+- No unused imports — `dart analyze` enforces this with `unused_import`
+
+## Code Generation
+
+- Generated files (`.g.dart`, `.freezed.dart`, `.gr.dart`) must be committed or gitignored consistently — pick one strategy per project
+- Never manually edit generated files
+- Keep generator annotations (`@JsonSerializable`, `@freezed`, `@riverpod`, etc.) on the canonical source file only
diff --git a/.claude/rules/ecc/dart/hooks.md b/.claude/rules/ecc/dart/hooks.md
new file mode 100644
index 0000000..d120efb
--- /dev/null
+++ b/.claude/rules/ecc/dart/hooks.md
@@ -0,0 +1,66 @@
+---
+paths:
+ - "**/*.dart"
+ - "**/pubspec.yaml"
+ - "**/analysis_options.yaml"
+---
+# Dart/Flutter Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Dart and Flutter-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **dart format**: Auto-format `.dart` files after edit
+- **dart analyze**: Run static analysis after editing Dart files and surface warnings
+- **flutter test**: Optionally run affected tests after significant changes
+
+## Recommended Hook Configuration
+
+```json
+{
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": { "tool_name": "Edit", "file_paths": ["**/*.dart"] },
+ "hooks": [
+ { "type": "command", "command": "dart format $CLAUDE_FILE_PATHS" }
+ ]
+ }
+ ]
+ }
+}
+```
+
+## Pre-commit Checks
+
+Run before committing Dart/Flutter changes:
+
+```bash
+dart format --set-exit-if-changed .
+dart analyze --fatal-infos
+flutter test
+```
+
+## Useful One-liners
+
+```bash
+# Format all Dart files
+dart format .
+
+# Analyze and report issues
+dart analyze
+
+# Run all tests with coverage
+flutter test --coverage
+
+# Regenerate code-gen files
+dart run build_runner build --delete-conflicting-outputs
+
+# Check for outdated packages
+flutter pub outdated
+
+# Upgrade packages within constraints
+flutter pub upgrade
+```
diff --git a/.claude/rules/ecc/dart/patterns.md b/.claude/rules/ecc/dart/patterns.md
new file mode 100644
index 0000000..94bf41b
--- /dev/null
+++ b/.claude/rules/ecc/dart/patterns.md
@@ -0,0 +1,261 @@
+---
+paths:
+ - "**/*.dart"
+ - "**/pubspec.yaml"
+---
+# Dart/Flutter Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Dart, Flutter, and common ecosystem-specific content.
+
+## Repository Pattern
+
+```dart
+abstract interface class UserRepository {
+ Future getById(String id);
+ Future> getAll();
+ Stream> watchAll();
+ Future save(User user);
+ Future delete(String id);
+}
+
+class UserRepositoryImpl implements UserRepository {
+ const UserRepositoryImpl(this._remote, this._local);
+
+ final UserRemoteDataSource _remote;
+ final UserLocalDataSource _local;
+
+ @override
+ Future getById(String id) async {
+ final local = await _local.getById(id);
+ if (local != null) return local;
+ final remote = await _remote.getById(id);
+ if (remote != null) await _local.save(remote);
+ return remote;
+ }
+
+ @override
+ Future> getAll() async {
+ final remote = await _remote.getAll();
+ for (final user in remote) {
+ await _local.save(user);
+ }
+ return remote;
+ }
+
+ @override
+ Stream> watchAll() => _local.watchAll();
+
+ @override
+ Future save(User user) => _local.save(user);
+
+ @override
+ Future delete(String id) async {
+ await _remote.delete(id);
+ await _local.delete(id);
+ }
+}
+```
+
+## State Management: BLoC/Cubit
+
+```dart
+// Cubit — simple state transitions
+class CounterCubit extends Cubit {
+ CounterCubit() : super(0);
+
+ void increment() => emit(state + 1);
+ void decrement() => emit(state - 1);
+}
+
+// BLoC — event-driven
+@immutable
+sealed class CartEvent {}
+class CartItemAdded extends CartEvent { CartItemAdded(this.item); final Item item; }
+class CartItemRemoved extends CartEvent { CartItemRemoved(this.id); final String id; }
+class CartCleared extends CartEvent {}
+
+@immutable
+class CartState {
+ const CartState({this.items = const []});
+ final List items;
+ CartState copyWith({List? items}) => CartState(items: items ?? this.items);
+}
+
+class CartBloc extends Bloc {
+ CartBloc() : super(const CartState()) {
+ on((event, emit) =>
+ emit(state.copyWith(items: [...state.items, event.item])));
+ on((event, emit) =>
+ emit(state.copyWith(items: state.items.where((i) => i.id != event.id).toList())));
+ on((_, emit) => emit(const CartState()));
+ }
+}
+```
+
+## State Management: Riverpod
+
+```dart
+// Simple provider
+@riverpod
+Future> users(Ref ref) async {
+ final repo = ref.watch(userRepositoryProvider);
+ return repo.getAll();
+}
+
+// Notifier for mutable state
+@riverpod
+class CartNotifier extends _$CartNotifier {
+ @override
+ List build() => [];
+
+ void add(Item item) => state = [...state, item];
+ void remove(String id) => state = state.where((i) => i.id != id).toList();
+ void clear() => state = [];
+}
+
+// ConsumerWidget
+class CartPage extends ConsumerWidget {
+ const CartPage({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final items = ref.watch(cartNotifierProvider);
+ return ListView(
+ children: items.map((item) => CartItemTile(item: item)).toList(),
+ );
+ }
+}
+```
+
+## Dependency Injection
+
+Constructor injection is preferred. Use `get_it` or Riverpod providers at composition root:
+
+```dart
+// get_it registration (in a setup file)
+void setupDependencies() {
+ final di = GetIt.instance;
+ di.registerSingleton(ApiClient(baseUrl: Env.apiUrl));
+ di.registerSingleton(
+ UserRepositoryImpl(di(), di()),
+ );
+ di.registerFactory(() => UserListViewModel(di()));
+}
+```
+
+## ViewModel Pattern (without BLoC/Riverpod)
+
+```dart
+class UserListViewModel extends ChangeNotifier {
+ UserListViewModel(this._repository);
+
+ final UserRepository _repository;
+
+ AsyncState> _state = const Loading();
+ AsyncState> get state => _state;
+
+ Future load() async {
+ _state = const Loading();
+ notifyListeners();
+ try {
+ final users = await _repository.getAll();
+ _state = Success(users);
+ } on Exception catch (e) {
+ _state = Failure(e);
+ }
+ notifyListeners();
+ }
+}
+```
+
+## UseCase Pattern
+
+```dart
+class GetUserUseCase {
+ const GetUserUseCase(this._repository);
+ final UserRepository _repository;
+
+ Future call(String id) => _repository.getById(id);
+}
+
+class CreateUserUseCase {
+ const CreateUserUseCase(this._repository, this._idGenerator);
+ final UserRepository _repository;
+ final IdGenerator _idGenerator; // injected — domain layer must not depend on uuid package directly
+
+ Future call(CreateUserInput input) async {
+ // Validate, apply business rules, then persist
+ final user = User(id: _idGenerator.generate(), name: input.name, email: input.email);
+ await _repository.save(user);
+ }
+}
+```
+
+## Immutable State with freezed
+
+```dart
+@freezed
+class UserState with _$UserState {
+ const factory UserState({
+ @Default([]) List users,
+ @Default(false) bool isLoading,
+ String? errorMessage,
+ }) = _UserState;
+}
+```
+
+## Clean Architecture Layer Boundaries
+
+```
+lib/
+├── domain/ # Pure Dart — no Flutter, no external packages
+│ ├── entities/
+│ ├── repositories/ # Abstract interfaces
+│ └── usecases/
+├── data/ # Implements domain interfaces
+│ ├── datasources/
+│ ├── models/ # DTOs with fromJson/toJson
+│ └── repositories/
+└── presentation/ # Flutter widgets + state management
+ ├── pages/
+ ├── widgets/
+ └── providers/ (or blocs/ or viewmodels/)
+```
+
+- Domain must not import `package:flutter` or any data-layer package
+- Data layer maps DTOs to domain entities at repository boundaries
+- Presentation calls use cases, not repositories directly
+
+## Navigation (GoRouter)
+
+```dart
+final router = GoRouter(
+ routes: [
+ GoRoute(
+ path: '/',
+ builder: (context, state) => const HomePage(),
+ ),
+ GoRoute(
+ path: '/users/:id',
+ builder: (context, state) {
+ final id = state.pathParameters['id']!;
+ return UserDetailPage(userId: id);
+ },
+ ),
+ ],
+ // refreshListenable re-evaluates redirect whenever auth state changes
+ refreshListenable: GoRouterRefreshStream(authCubit.stream),
+ redirect: (context, state) {
+ final isLoggedIn = context.read().state is AuthAuthenticated;
+ if (!isLoggedIn && !state.matchedLocation.startsWith('/login')) {
+ return '/login';
+ }
+ return null;
+ },
+);
+```
+
+## References
+
+See skill: `flutter-dart-code-review` for the comprehensive review checklist.
+See skill: `compose-multiplatform-patterns` for Kotlin Multiplatform/Flutter interop patterns.
diff --git a/.claude/rules/ecc/dart/security.md b/.claude/rules/ecc/dart/security.md
new file mode 100644
index 0000000..74d9643
--- /dev/null
+++ b/.claude/rules/ecc/dart/security.md
@@ -0,0 +1,135 @@
+---
+paths:
+ - "**/*.dart"
+ - "**/pubspec.yaml"
+ - "**/AndroidManifest.xml"
+ - "**/Info.plist"
+---
+# Dart/Flutter Security
+
+> This file extends [common/security.md](../common/security.md) with Dart, Flutter, and mobile-specific content.
+
+## Secrets Management
+
+- Never hardcode API keys, tokens, or credentials in Dart source
+- Use `--dart-define` or `--dart-define-from-file` for compile-time config (values are not truly secret — use a backend proxy for server-side secrets)
+- Use `flutter_dotenv` or equivalent, with `.env` files listed in `.gitignore`
+- Store runtime secrets in platform-secure storage: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android)
+
+```dart
+// BAD
+const apiKey = 'sk-abc123...';
+
+// GOOD — compile-time config (not secret, just configurable)
+const apiKey = String.fromEnvironment('API_KEY');
+
+// GOOD — runtime secret from secure storage
+final token = await secureStorage.read(key: 'auth_token');
+```
+
+## Network Security
+
+- Enforce HTTPS — no `http://` calls in production
+- Configure Android `network_security_config.xml` to block cleartext traffic
+- Set `NSAppTransportSecurity` in `Info.plist` to disallow arbitrary loads
+- Set request timeouts on all HTTP clients — never leave defaults
+- Consider certificate pinning for high-security endpoints
+
+```dart
+// Dio with timeout and HTTPS enforcement
+final dio = Dio(BaseOptions(
+ baseUrl: 'https://api.example.com',
+ connectTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 30),
+));
+```
+
+## Input Validation
+
+- Validate and sanitize all user input before sending to API or storage
+- Never pass unsanitized input to SQL queries — use parameterized queries (sqflite, drift)
+- Sanitize deep link URLs before navigation — validate scheme, host, and path parameters
+- Use `Uri.tryParse` and validate before navigating
+
+```dart
+// BAD — SQL injection
+await db.rawQuery("SELECT * FROM users WHERE email = '$userInput'");
+
+// GOOD — parameterized
+await db.query('users', where: 'email = ?', whereArgs: [userInput]);
+
+// BAD — unvalidated deep link
+final uri = Uri.parse(incomingLink);
+context.go(uri.path); // could navigate to any route
+
+// GOOD — validated deep link
+final uri = Uri.tryParse(incomingLink);
+if (uri != null && uri.host == 'myapp.com' && _allowedPaths.contains(uri.path)) {
+ context.go(uri.path);
+}
+```
+
+## Data Protection
+
+- Store tokens, PII, and credentials only in `flutter_secure_storage`
+- Never write sensitive data to `SharedPreferences` or local files in plaintext
+- Clear auth state on logout: tokens, cached user data, cookies
+- Use biometric authentication (`local_auth`) for sensitive operations
+- Avoid logging sensitive data — no `print(token)` or `debugPrint(password)`
+
+## Android-Specific
+
+- Declare only required permissions in `AndroidManifest.xml`
+- Export Android components (`Activity`, `Service`, `BroadcastReceiver`) only when necessary; add `android:exported="false"` where not needed
+- Review intent filters — exported components with implicit intent filters are accessible by any app
+- Use `FLAG_SECURE` for screens displaying sensitive data (prevents screenshots)
+
+```xml
+
+
+
+
+
+```
+
+## iOS-Specific
+
+- Declare only required usage descriptions in `Info.plist` (`NSCameraUsageDescription`, etc.)
+- Store secrets in Keychain — `flutter_secure_storage` uses Keychain on iOS
+- Use App Transport Security (ATS) — disallow arbitrary loads
+- Enable data protection entitlement for sensitive files
+
+## WebView Security
+
+- Use `webview_flutter` v4+ (`WebViewController` / `WebViewWidget`) — the legacy `WebView` widget is removed
+- Disable JavaScript unless explicitly required (`JavaScriptMode.disabled`)
+- Validate URLs before loading — never load arbitrary URLs from deep links
+- Never expose Dart callbacks to JavaScript unless absolutely needed and carefully sandboxed
+- Use `NavigationDelegate.onNavigationRequest` to intercept and validate navigation requests
+
+```dart
+// webview_flutter v4+ API (WebViewController + WebViewWidget)
+final controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.disabled) // disabled unless required
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onNavigationRequest: (request) {
+ final uri = Uri.tryParse(request.url);
+ if (uri == null || uri.host != 'trusted.example.com') {
+ return NavigationDecision.prevent;
+ }
+ return NavigationDecision.navigate;
+ },
+ ),
+ );
+
+// In your widget tree:
+WebViewWidget(controller: controller)
+```
+
+## Obfuscation and Build Security
+
+- Enable obfuscation in release builds: `flutter build apk --obfuscate --split-debug-info=./debug-info/`
+- Keep `--split-debug-info` output out of version control (used for crash symbolication only)
+- Ensure ProGuard/R8 rules don't inadvertently expose serialized classes
+- Run `flutter analyze` and address all warnings before release
diff --git a/.claude/rules/ecc/dart/testing.md b/.claude/rules/ecc/dart/testing.md
new file mode 100644
index 0000000..bd6ff62
--- /dev/null
+++ b/.claude/rules/ecc/dart/testing.md
@@ -0,0 +1,215 @@
+---
+paths:
+ - "**/*.dart"
+ - "**/pubspec.yaml"
+ - "**/analysis_options.yaml"
+---
+# Dart/Flutter Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Dart and Flutter-specific content.
+
+## Test Framework
+
+- **flutter_test** / **dart:test** — built-in test runner
+- **mockito** (with `@GenerateMocks`) or **mocktail** (no codegen) for mocking
+- **bloc_test** for BLoC/Cubit unit tests
+- **fake_async** for controlling time in unit tests
+- **integration_test** for end-to-end device tests
+
+## Test Types
+
+| Type | Tool | Location | When to Write |
+|------|------|----------|---------------|
+| Unit | `dart:test` | `test/unit/` | All domain logic, state managers, repositories |
+| Widget | `flutter_test` | `test/widget/` | All widgets with meaningful behavior |
+| Golden | `flutter_test` | `test/golden/` | Design-critical UI components |
+| Integration | `integration_test` | `integration_test/` | Critical user flows on real device/emulator |
+
+## Unit Tests: State Managers
+
+### BLoC with `bloc_test`
+
+```dart
+group('CartBloc', () {
+ late CartBloc bloc;
+ late MockCartRepository repository;
+
+ setUp(() {
+ repository = MockCartRepository();
+ bloc = CartBloc(repository);
+ });
+
+ tearDown(() => bloc.close());
+
+ blocTest(
+ 'emits updated items when CartItemAdded',
+ build: () => bloc,
+ act: (b) => b.add(CartItemAdded(testItem)),
+ expect: () => [CartState(items: [testItem])],
+ );
+
+ blocTest(
+ 'emits empty cart when CartCleared',
+ seed: () => CartState(items: [testItem]),
+ build: () => bloc,
+ act: (b) => b.add(CartCleared()),
+ expect: () => [const CartState()],
+ );
+});
+```
+
+### Riverpod with `ProviderContainer`
+
+```dart
+test('usersProvider loads users from repository', () async {
+ final container = ProviderContainer(
+ overrides: [userRepositoryProvider.overrideWithValue(FakeUserRepository())],
+ );
+ addTearDown(container.dispose);
+
+ final result = await container.read(usersProvider.future);
+ expect(result, isNotEmpty);
+});
+```
+
+## Widget Tests
+
+```dart
+testWidgets('CartPage shows item count badge', (tester) async {
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ cartNotifierProvider.overrideWith(() => FakeCartNotifier([testItem])),
+ ],
+ child: const MaterialApp(home: CartPage()),
+ ),
+ );
+
+ await tester.pump();
+ expect(find.text('1'), findsOneWidget);
+ expect(find.byType(CartItemTile), findsOneWidget);
+});
+
+testWidgets('shows empty state when cart is empty', (tester) async {
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier([]))],
+ child: const MaterialApp(home: CartPage()),
+ ),
+ );
+
+ await tester.pump();
+ expect(find.text('Your cart is empty'), findsOneWidget);
+});
+```
+
+## Fakes Over Mocks
+
+Prefer hand-written fakes for complex dependencies:
+
+```dart
+class FakeUserRepository implements UserRepository {
+ final _users = {};
+ Object? fetchError;
+
+ @override
+ Future getById(String id) async {
+ if (fetchError != null) throw fetchError!;
+ return _users[id];
+ }
+
+ @override
+ Future> getAll() async {
+ if (fetchError != null) throw fetchError!;
+ return _users.values.toList();
+ }
+
+ @override
+ Stream> watchAll() => Stream.value(_users.values.toList());
+
+ @override
+ Future save(User user) async {
+ _users[user.id] = user;
+ }
+
+ @override
+ Future delete(String id) async {
+ _users.remove(id);
+ }
+
+ void addUser(User user) => _users[user.id] = user;
+}
+```
+
+## Async Testing
+
+```dart
+// Use fake_async for controlling timers and Futures
+test('debounce triggers after 300ms', () {
+ fakeAsync((async) {
+ final debouncer = Debouncer(delay: const Duration(milliseconds: 300));
+ var callCount = 0;
+ debouncer.run(() => callCount++);
+ expect(callCount, 0);
+ async.elapse(const Duration(milliseconds: 200));
+ expect(callCount, 0);
+ async.elapse(const Duration(milliseconds: 200));
+ expect(callCount, 1);
+ });
+});
+```
+
+## Golden Tests
+
+```dart
+testWidgets('UserCard golden test', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(home: UserCard(user: testUser)),
+ );
+
+ await expectLater(
+ find.byType(UserCard),
+ matchesGoldenFile('goldens/user_card.png'),
+ );
+});
+```
+
+Run `flutter test --update-goldens` when intentional visual changes are made.
+
+## Test Naming
+
+Use descriptive, behavior-focused names:
+
+```dart
+test('returns null when user does not exist', () { ... });
+test('throws NotFoundException when id is empty string', () { ... });
+testWidgets('disables submit button while form is invalid', (tester) async { ... });
+```
+
+## Test Organization
+
+```
+test/
+├── unit/
+│ ├── domain/
+│ │ └── usecases/
+│ └── data/
+│ └── repositories/
+├── widget/
+│ └── presentation/
+│ └── pages/
+└── golden/
+ └── widgets/
+
+integration_test/
+└── flows/
+ ├── login_flow_test.dart
+ └── checkout_flow_test.dart
+```
+
+## Coverage
+
+- Target 80%+ line coverage for business logic (domain + state managers)
+- All state transitions must have tests: loading → success, loading → error, retry
+- Run `flutter test --coverage` and inspect `lcov.info` with a coverage reporter
+- Coverage failures should block CI when below threshold
diff --git a/.claude/rules/ecc/fsharp/coding-style.md b/.claude/rules/ecc/fsharp/coding-style.md
new file mode 100644
index 0000000..89e2977
--- /dev/null
+++ b/.claude/rules/ecc/fsharp/coding-style.md
@@ -0,0 +1,112 @@
+---
+paths:
+ - "**/*.fs"
+ - "**/*.fsx"
+---
+# F# Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with F#-specific content.
+
+## Standards
+
+- Follow standard F# conventions and leverage the type system for correctness
+- Prefer immutability by default; use `mutable` only when justified by performance
+- Keep modules focused and cohesive
+
+## Types and Models
+
+- Prefer discriminated unions for domain modeling over class hierarchies
+- Use records for data with named fields
+- Use single-case unions for type-safe wrappers around primitives
+- Avoid classes unless interop or mutable state requires them
+
+```fsharp
+type EmailAddress = EmailAddress of string
+
+type OrderStatus =
+ | Pending
+ | Confirmed of confirmedAt: DateTimeOffset
+ | Shipped of trackingNumber: string
+ | Cancelled of reason: string
+
+type Order =
+ { Id: Guid
+ CustomerId: string
+ Status: OrderStatus
+ Items: OrderItem list }
+```
+
+## Immutability
+
+- Records are immutable by default; use `with` expressions for updates
+- Prefer `list`, `map`, `set` over mutable collections
+- Avoid `ref` cells and mutable fields in domain logic
+
+```fsharp
+let rename (profile: UserProfile) newName =
+ { profile with Name = newName }
+```
+
+## Function Style
+
+- Prefer small, composable functions over large methods
+- Use the pipe operator `|>` to build readable data pipelines
+- Prefer pattern matching over if/else chains
+- Use `Option` instead of null; use `Result` for operations that can fail
+
+```fsharp
+let processOrder order =
+ order
+ |> validateItems
+ |> Result.bind calculateTotal
+ |> Result.map applyDiscount
+ |> Result.mapError OrderError
+```
+
+## Async and Error Handling
+
+- Use `task { }` for interop with .NET async APIs
+- Use `async { }` for F#-native async workflows
+- Propagate `CancellationToken` through public async APIs
+- Prefer `Result` and railway-oriented programming over exceptions for expected failures
+
+```fsharp
+let loadOrderAsync (orderId: Guid) (ct: CancellationToken) =
+ task {
+ let! order = repository.FindAsync(orderId, ct)
+ return
+ order
+ |> Option.defaultWith (fun () ->
+ failwith $"Order {orderId} was not found.")
+ }
+```
+
+## Formatting
+
+- Use `fantomas` for automatic formatting
+- Prefer significant whitespace; avoid unnecessary parentheses
+- Remove unused `open` declarations
+
+### Open Declaration Order
+
+Group `open` statements into four sections separated by a blank line, each section sorted lexically within itself:
+
+1. `System.*`
+2. `Microsoft.*`
+3. Third-party namespaces
+4. First-party / project namespaces
+
+```fsharp
+open System
+open System.Collections.Generic
+open System.Threading.Tasks
+
+open Microsoft.AspNetCore.Http
+open Microsoft.Extensions.Logging
+
+open FsCheck.Xunit
+open Swensen.Unquote
+
+open MyApp.Domain
+open MyApp.Infrastructure
+```
diff --git a/.claude/rules/ecc/fsharp/hooks.md b/.claude/rules/ecc/fsharp/hooks.md
new file mode 100644
index 0000000..9108d20
--- /dev/null
+++ b/.claude/rules/ecc/fsharp/hooks.md
@@ -0,0 +1,26 @@
+---
+paths:
+ - "**/*.fs"
+ - "**/*.fsx"
+ - "**/*.fsproj"
+ - "**/*.sln"
+ - "**/*.slnx"
+ - "**/Directory.Build.props"
+ - "**/Directory.Build.targets"
+---
+# F# Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with F#-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **fantomas**: Auto-format edited F# files
+- **dotnet build**: Verify the solution or project still compiles after edits
+- **dotnet test --no-build**: Re-run the nearest relevant test project after behavior changes
+
+## Stop Hooks
+
+- Run a final `dotnet build` before ending a session with broad F# changes
+- Warn on modified `appsettings*.json` files so secrets do not get committed
diff --git a/.claude/rules/ecc/fsharp/patterns.md b/.claude/rules/ecc/fsharp/patterns.md
new file mode 100644
index 0000000..490b995
--- /dev/null
+++ b/.claude/rules/ecc/fsharp/patterns.md
@@ -0,0 +1,111 @@
+---
+paths:
+ - "**/*.fs"
+ - "**/*.fsx"
+---
+# F# Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with F#-specific content.
+
+## Result Type for Error Handling
+
+Use `Result<'T, 'TError>` with railway-oriented programming instead of exceptions for expected failures.
+
+```fsharp
+type OrderError =
+ | InvalidCustomer of string
+ | EmptyItems
+ | ItemOutOfStock of sku: string
+
+let validateOrder (request: CreateOrderRequest) : Result =
+ if String.IsNullOrWhiteSpace request.CustomerId then
+ Error(InvalidCustomer "CustomerId is required")
+ elif request.Items |> List.isEmpty then
+ Error EmptyItems
+ else
+ Ok { CustomerId = request.CustomerId; Items = request.Items }
+```
+
+## Option for Missing Values
+
+Prefer `Option<'T>` over null. Use `Option.map`, `Option.bind`, and `Option.defaultValue` to transform.
+
+```fsharp
+let findUser (id: Guid) : User option =
+ users |> Map.tryFind id
+
+let getUserEmail userId =
+ findUser userId
+ |> Option.map (fun u -> u.Email)
+ |> Option.defaultValue "unknown@example.com"
+```
+
+## Discriminated Unions for Domain Modeling
+
+Model business states explicitly. The compiler enforces exhaustive handling.
+
+```fsharp
+type PaymentState =
+ | AwaitingPayment of amount: decimal
+ | Paid of paidAt: DateTimeOffset * transactionId: string
+ | Refunded of refundedAt: DateTimeOffset * reason: string
+ | Failed of error: string
+
+let describePayment = function
+ | AwaitingPayment amount -> $"Awaiting payment of {amount:C}"
+ | Paid (at, txn) -> $"Paid at {at} (txn: {txn})"
+ | Refunded (at, reason) -> $"Refunded at {at}: {reason}"
+ | Failed error -> $"Payment failed: {error}"
+```
+
+## Computation Expressions
+
+Use computation expressions to simplify sequential operations that may fail.
+
+```fsharp
+let placeOrder request =
+ result {
+ let! validated = validateOrder request
+ let! inventory = checkInventory validated.Items
+ let! order = createOrder validated inventory
+ return order
+ }
+```
+
+## Module Organization
+
+- Group related functions in modules rather than classes
+- Use `[]` to prevent name collisions
+- Keep modules small and focused on a single responsibility
+
+```fsharp
+[]
+module Order =
+ let create customerId items = { Id = Guid.NewGuid(); CustomerId = customerId; Items = items; Status = Pending }
+ let confirm order = { order with Status = Confirmed(DateTimeOffset.UtcNow) }
+ let cancel reason order = { order with Status = Cancelled reason }
+```
+
+## Dependency Injection
+
+- Define dependencies as function parameters or record-of-functions
+- Use interfaces sparingly, primarily at the boundary with .NET libraries
+- Prefer partial application for injecting dependencies into pipelines
+
+```fsharp
+type OrderDeps =
+ { FindOrder: Guid -> Task
+ SaveOrder: Order -> Task
+ SendNotification: Order -> Task }
+
+let processOrder (deps: OrderDeps) orderId =
+ task {
+ match! deps.FindOrder orderId with
+ | None -> return Error "Order not found"
+ | Some order ->
+ let confirmed = Order.confirm order
+ do! deps.SaveOrder confirmed
+ do! deps.SendNotification confirmed
+ return Ok confirmed
+ }
+```
diff --git a/.claude/rules/ecc/fsharp/security.md b/.claude/rules/ecc/fsharp/security.md
new file mode 100644
index 0000000..86801c0
--- /dev/null
+++ b/.claude/rules/ecc/fsharp/security.md
@@ -0,0 +1,76 @@
+---
+paths:
+ - "**/*.fs"
+ - "**/*.fsx"
+ - "**/*.fsproj"
+ - "**/appsettings*.json"
+---
+# F# Security
+
+> This file extends [common/security.md](../common/security.md) with F#-specific content.
+
+## Secret Management
+
+- Never hardcode API keys, tokens, or connection strings in source code
+- Use environment variables, user secrets for local development, and a secret manager in production
+- Keep `appsettings.*.json` free of real credentials
+
+```fsharp
+// BAD
+let apiKey = "sk-live-123"
+
+// GOOD
+let apiKey =
+ configuration["OpenAI:ApiKey"]
+ |> Option.ofObj
+ |> Option.defaultWith (fun () -> failwith "OpenAI:ApiKey is not configured.")
+```
+
+## SQL Injection Prevention
+
+- Always use parameterized queries with ADO.NET, Dapper, or EF Core
+- Never concatenate user input into SQL strings
+- Validate sort fields and filter operators before using dynamic query composition
+
+```fsharp
+let findByCustomer (connection: IDbConnection) customerId =
+ task {
+ let sql = "SELECT * FROM Orders WHERE CustomerId = @customerId"
+ return! connection.QueryAsync(sql, {| customerId = customerId |})
+ }
+```
+
+## Input Validation
+
+- Validate inputs at the application boundary using types
+- Use single-case discriminated unions for validated values
+- Reject invalid input before it enters domain logic
+
+```fsharp
+type ValidatedEmail = private ValidatedEmail of string
+
+module ValidatedEmail =
+ let create (input: string) =
+ if System.Text.RegularExpressions.Regex.IsMatch(input, @"^[^@]+@[^@]+\.[^@]+$") then
+ Ok(ValidatedEmail input)
+ else
+ Error "Invalid email address"
+
+ let value (ValidatedEmail v) = v
+```
+
+## Authentication and Authorization
+
+- Prefer framework auth handlers instead of custom token parsing
+- Enforce authorization policies at endpoint or handler boundaries
+- Never log raw tokens, passwords, or PII
+
+## Error Handling
+
+- Return safe client-facing messages
+- Log detailed exceptions with structured context server-side
+- Do not expose stack traces, SQL text, or filesystem paths in API responses
+
+## References
+
+See skill: `security-review` for broader application security review checklists.
diff --git a/.claude/rules/ecc/fsharp/testing.md b/.claude/rules/ecc/fsharp/testing.md
new file mode 100644
index 0000000..8dbc7f9
--- /dev/null
+++ b/.claude/rules/ecc/fsharp/testing.md
@@ -0,0 +1,62 @@
+---
+paths:
+ - "**/*.fs"
+ - "**/*.fsx"
+ - "**/*.fsproj"
+---
+# F# Testing
+
+> This file extends [common/testing.md](../common/testing.md) with F#-specific content.
+
+## Test Framework
+
+- Prefer **xUnit** with **FsUnit.xUnit** for F#-friendly assertions
+- Use **Unquote** for quotation-based assertions with clear failure messages
+- Use **FsCheck.xUnit** for property-based testing
+- Use **NSubstitute** or function stubs for mocking dependencies
+- Use **Testcontainers** when integration tests need real infrastructure
+
+## Test Organization
+
+- Mirror `src/` structure under `tests/`
+- Separate unit, integration, and end-to-end coverage clearly
+- Name tests by behavior, not implementation details
+
+```fsharp
+open Xunit
+open Swensen.Unquote
+
+[]
+let ``PlaceOrder returns success when request is valid`` () =
+ let request = { CustomerId = "cust-123"; Items = [ validItem ] }
+ let result = OrderService.placeOrder request
+ test <@ Result.isOk result @>
+
+[]
+let ``PlaceOrder returns error when items are empty`` () =
+ let request = { CustomerId = "cust-123"; Items = [] }
+ let result = OrderService.placeOrder request
+ test <@ Result.isError result @>
+```
+
+## Property-Based Testing with FsCheck
+
+```fsharp
+open FsCheck.Xunit
+
+[]
+let ``order total is never negative`` (items: OrderItem list) =
+ let total = Order.calculateTotal items
+ total >= 0m
+```
+
+## ASP.NET Core Integration Tests
+
+- Use `WebApplicationFactory` for API integration coverage
+- Test auth, validation, and serialization through HTTP, not by bypassing middleware
+
+## Coverage
+
+- Target 80%+ line coverage
+- Focus coverage on domain logic, validation, auth, and failure paths
+- Run `dotnet test` in CI with coverage collection enabled where available
diff --git a/.claude/rules/ecc/golang/coding-style.md b/.claude/rules/ecc/golang/coding-style.md
new file mode 100644
index 0000000..d7d6c31
--- /dev/null
+++ b/.claude/rules/ecc/golang/coding-style.md
@@ -0,0 +1,32 @@
+---
+paths:
+ - "**/*.go"
+ - "**/go.mod"
+ - "**/go.sum"
+---
+# Go Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Go specific content.
+
+## Formatting
+
+- **gofmt** and **goimports** are mandatory — no style debates
+
+## Design Principles
+
+- Accept interfaces, return structs
+- Keep interfaces small (1-3 methods)
+
+## Error Handling
+
+Always wrap errors with context:
+
+```go
+if err != nil {
+ return fmt.Errorf("failed to create user: %w", err)
+}
+```
+
+## Reference
+
+See skill: `golang-patterns` for comprehensive Go idioms and patterns.
diff --git a/.claude/rules/ecc/golang/hooks.md b/.claude/rules/ecc/golang/hooks.md
new file mode 100644
index 0000000..f05e4ad
--- /dev/null
+++ b/.claude/rules/ecc/golang/hooks.md
@@ -0,0 +1,17 @@
+---
+paths:
+ - "**/*.go"
+ - "**/go.mod"
+ - "**/go.sum"
+---
+# Go Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Go specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **gofmt/goimports**: Auto-format `.go` files after edit
+- **go vet**: Run static analysis after editing `.go` files
+- **staticcheck**: Run extended static checks on modified packages
diff --git a/.claude/rules/ecc/golang/patterns.md b/.claude/rules/ecc/golang/patterns.md
new file mode 100644
index 0000000..ba28dba
--- /dev/null
+++ b/.claude/rules/ecc/golang/patterns.md
@@ -0,0 +1,45 @@
+---
+paths:
+ - "**/*.go"
+ - "**/go.mod"
+ - "**/go.sum"
+---
+# Go Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Go specific content.
+
+## Functional Options
+
+```go
+type Option func(*Server)
+
+func WithPort(port int) Option {
+ return func(s *Server) { s.port = port }
+}
+
+func NewServer(opts ...Option) *Server {
+ s := &Server{port: 8080}
+ for _, opt := range opts {
+ opt(s)
+ }
+ return s
+}
+```
+
+## Small Interfaces
+
+Define interfaces where they are used, not where they are implemented.
+
+## Dependency Injection
+
+Use constructor functions to inject dependencies:
+
+```go
+func NewUserService(repo UserRepository, logger Logger) *UserService {
+ return &UserService{repo: repo, logger: logger}
+}
+```
+
+## Reference
+
+See skill: `golang-patterns` for comprehensive Go patterns including concurrency, error handling, and package organization.
diff --git a/.claude/rules/ecc/golang/security.md b/.claude/rules/ecc/golang/security.md
new file mode 100644
index 0000000..372b754
--- /dev/null
+++ b/.claude/rules/ecc/golang/security.md
@@ -0,0 +1,34 @@
+---
+paths:
+ - "**/*.go"
+ - "**/go.mod"
+ - "**/go.sum"
+---
+# Go Security
+
+> This file extends [common/security.md](../common/security.md) with Go specific content.
+
+## Secret Management
+
+```go
+apiKey := os.Getenv("OPENAI_API_KEY")
+if apiKey == "" {
+ log.Fatal("OPENAI_API_KEY not configured")
+}
+```
+
+## Security Scanning
+
+- Use **gosec** for static security analysis:
+ ```bash
+ gosec ./...
+ ```
+
+## Context & Timeouts
+
+Always use `context.Context` for timeout control:
+
+```go
+ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+defer cancel()
+```
diff --git a/.claude/rules/ecc/golang/testing.md b/.claude/rules/ecc/golang/testing.md
new file mode 100644
index 0000000..6b80022
--- /dev/null
+++ b/.claude/rules/ecc/golang/testing.md
@@ -0,0 +1,31 @@
+---
+paths:
+ - "**/*.go"
+ - "**/go.mod"
+ - "**/go.sum"
+---
+# Go Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Go specific content.
+
+## Framework
+
+Use the standard `go test` with **table-driven tests**.
+
+## Race Detection
+
+Always run with the `-race` flag:
+
+```bash
+go test -race ./...
+```
+
+## Coverage
+
+```bash
+go test -cover ./...
+```
+
+## Reference
+
+See skill: `golang-testing` for detailed Go testing patterns and helpers.
diff --git a/.claude/rules/ecc/java/coding-style.md b/.claude/rules/ecc/java/coding-style.md
new file mode 100644
index 0000000..d20d5ab
--- /dev/null
+++ b/.claude/rules/ecc/java/coding-style.md
@@ -0,0 +1,114 @@
+---
+paths:
+ - "**/*.java"
+---
+# Java Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Java-specific content.
+
+## Formatting
+
+- **google-java-format** or **Checkstyle** (Google or Sun style) for enforcement
+- One public top-level type per file
+- Consistent indent: 2 or 4 spaces (match project standard)
+- Member order: constants, fields, constructors, public methods, protected, private
+
+## Immutability
+
+- Prefer `record` for value types (Java 16+)
+- Mark fields `final` by default — use mutable state only when required
+- Return defensive copies from public APIs: `List.copyOf()`, `Map.copyOf()`, `Set.copyOf()`
+- Copy-on-write: return new instances rather than mutating existing ones
+
+```java
+// GOOD — immutable value type
+public record OrderSummary(Long id, String customerName, BigDecimal total) {}
+
+// GOOD — final fields, no setters
+public class Order {
+ private final Long id;
+ private final List items;
+
+ public List getItems() {
+ return List.copyOf(items);
+ }
+}
+```
+
+## Naming
+
+Follow standard Java conventions:
+- `PascalCase` for classes, interfaces, records, enums
+- `camelCase` for methods, fields, parameters, local variables
+- `SCREAMING_SNAKE_CASE` for `static final` constants
+- Packages: all lowercase, reverse domain (`com.example.app.service`)
+
+## Modern Java Features
+
+Use modern language features where they improve clarity:
+- **Records** for DTOs and value types (Java 16+)
+- **Sealed classes** for closed type hierarchies (Java 17+)
+- **Pattern matching** with `instanceof` — no explicit cast (Java 16+)
+- **Text blocks** for multi-line strings — SQL, JSON templates (Java 15+)
+- **Switch expressions** with arrow syntax (Java 14+)
+- **Pattern matching in switch** — exhaustive sealed type handling (Java 21+)
+
+```java
+// Pattern matching instanceof
+if (shape instanceof Circle c) {
+ return Math.PI * c.radius() * c.radius();
+}
+
+// Sealed type hierarchy
+public sealed interface PaymentMethod permits CreditCard, BankTransfer, Wallet {}
+
+// Switch expression
+String label = switch (status) {
+ case ACTIVE -> "Active";
+ case SUSPENDED -> "Suspended";
+ case CLOSED -> "Closed";
+};
+```
+
+## Optional Usage
+
+- Return `Optional` from finder methods that may have no result
+- Use `map()`, `flatMap()`, `orElseThrow()` — never call `get()` without `isPresent()`
+- Never use `Optional` as a field type or method parameter
+
+```java
+// GOOD
+return repository.findById(id)
+ .map(ResponseDto::from)
+ .orElseThrow(() -> new OrderNotFoundException(id));
+
+// BAD — Optional as parameter
+public void process(Optional name) {}
+```
+
+## Error Handling
+
+- Prefer unchecked exceptions for domain errors
+- Create domain-specific exceptions extending `RuntimeException`
+- Avoid broad `catch (Exception e)` unless at top-level handlers
+- Include context in exception messages
+
+```java
+public class OrderNotFoundException extends RuntimeException {
+ public OrderNotFoundException(Long id) {
+ super("Order not found: id=" + id);
+ }
+}
+```
+
+## Streams
+
+- Use streams for transformations; keep pipelines short (3-4 operations max)
+- Prefer method references when readable: `.map(Order::getTotal)`
+- Avoid side effects in stream operations
+- For complex logic, prefer a loop over a convoluted stream pipeline
+
+## References
+
+See skill: `java-coding-standards` for full coding standards with examples.
+See skill: `jpa-patterns` for JPA/Hibernate entity design patterns.
diff --git a/.claude/rules/ecc/java/hooks.md b/.claude/rules/ecc/java/hooks.md
new file mode 100644
index 0000000..9dd33b3
--- /dev/null
+++ b/.claude/rules/ecc/java/hooks.md
@@ -0,0 +1,18 @@
+---
+paths:
+ - "**/*.java"
+ - "**/pom.xml"
+ - "**/build.gradle"
+ - "**/build.gradle.kts"
+---
+# Java Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Java-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **google-java-format**: Auto-format `.java` files after edit
+- **checkstyle**: Run style checks after editing Java files
+- **./mvnw compile** or **./gradlew compileJava**: Verify compilation after changes
diff --git a/.claude/rules/ecc/java/patterns.md b/.claude/rules/ecc/java/patterns.md
new file mode 100644
index 0000000..44ed3f2
--- /dev/null
+++ b/.claude/rules/ecc/java/patterns.md
@@ -0,0 +1,147 @@
+---
+paths:
+ - "**/*.java"
+---
+# Java Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Java-specific content.
+
+## Repository Pattern
+
+Encapsulate data access behind an interface:
+
+```java
+public interface OrderRepository {
+ Optional findById(Long id);
+ List findAll();
+ Order save(Order order);
+ void deleteById(Long id);
+}
+```
+
+Concrete implementations handle storage details (JPA, JDBC, in-memory for tests).
+
+## Service Layer
+
+Business logic in service classes; keep controllers and repositories thin:
+
+```java
+public class OrderService {
+ private final OrderRepository orderRepository;
+ private final PaymentGateway paymentGateway;
+
+ public OrderService(OrderRepository orderRepository, PaymentGateway paymentGateway) {
+ this.orderRepository = orderRepository;
+ this.paymentGateway = paymentGateway;
+ }
+
+ public OrderSummary placeOrder(CreateOrderRequest request) {
+ var order = Order.from(request);
+ paymentGateway.charge(order.total());
+ var saved = orderRepository.save(order);
+ return OrderSummary.from(saved);
+ }
+}
+```
+
+## Constructor Injection
+
+Always use constructor injection — never field injection:
+
+```java
+// GOOD — constructor injection (testable, immutable)
+public class NotificationService {
+ private final EmailSender emailSender;
+
+ public NotificationService(EmailSender emailSender) {
+ this.emailSender = emailSender;
+ }
+}
+
+// BAD — field injection (untestable without reflection, requires framework magic)
+public class NotificationService {
+ @Inject // or @Autowired
+ private EmailSender emailSender;
+}
+```
+
+## DTO Mapping
+
+Use records for DTOs. Map at service/controller boundaries:
+
+```java
+public record OrderResponse(Long id, String customer, BigDecimal total) {
+ public static OrderResponse from(Order order) {
+ return new OrderResponse(order.getId(), order.getCustomerName(), order.getTotal());
+ }
+}
+```
+
+## Builder Pattern
+
+Use for objects with many optional parameters:
+
+```java
+public class SearchCriteria {
+ private final String query;
+ private final int page;
+ private final int size;
+ private final String sortBy;
+
+ private SearchCriteria(Builder builder) {
+ this.query = builder.query;
+ this.page = builder.page;
+ this.size = builder.size;
+ this.sortBy = builder.sortBy;
+ }
+
+ public static class Builder {
+ private String query = "";
+ private int page = 0;
+ private int size = 20;
+ private String sortBy = "id";
+
+ public Builder query(String query) { this.query = query; return this; }
+ public Builder page(int page) { this.page = page; return this; }
+ public Builder size(int size) { this.size = size; return this; }
+ public Builder sortBy(String sortBy) { this.sortBy = sortBy; return this; }
+ public SearchCriteria build() { return new SearchCriteria(this); }
+ }
+}
+```
+
+## Sealed Types for Domain Models
+
+```java
+public sealed interface PaymentResult permits PaymentSuccess, PaymentFailure {
+ record PaymentSuccess(String transactionId, BigDecimal amount) implements PaymentResult {}
+ record PaymentFailure(String errorCode, String message) implements PaymentResult {}
+}
+
+// Exhaustive handling (Java 21+)
+String message = switch (result) {
+ case PaymentSuccess s -> "Paid: " + s.transactionId();
+ case PaymentFailure f -> "Failed: " + f.errorCode();
+};
+```
+
+## API Response Envelope
+
+Consistent API responses:
+
+```java
+public record ApiResponse(boolean success, T data, String error) {
+ public static ApiResponse ok(T data) {
+ return new ApiResponse<>(true, data, null);
+ }
+ public static ApiResponse error(String message) {
+ return new ApiResponse<>(false, null, message);
+ }
+}
+```
+
+## References
+
+See skill: `springboot-patterns` for Spring Boot architecture patterns.
+See skill: `quarkus-patterns` for Quarkus architecture patterns with REST, Panache, and messaging.
+See skill: `jpa-patterns` for entity design and query optimization.
diff --git a/.claude/rules/ecc/java/security.md b/.claude/rules/ecc/java/security.md
new file mode 100644
index 0000000..cbedc12
--- /dev/null
+++ b/.claude/rules/ecc/java/security.md
@@ -0,0 +1,101 @@
+---
+paths:
+ - "**/*.java"
+---
+# Java Security
+
+> This file extends [common/security.md](../common/security.md) with Java-specific content.
+
+## Secrets Management
+
+- Never hardcode API keys, tokens, or credentials in source code
+- Use environment variables: `System.getenv("API_KEY")`
+- Use a secret manager (Vault, AWS Secrets Manager) for production secrets
+- Keep local config files with secrets in `.gitignore`
+
+```java
+// BAD
+private static final String API_KEY = "sk-abc123...";
+
+// GOOD — environment variable
+String apiKey = System.getenv("PAYMENT_API_KEY");
+Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
+```
+
+## SQL Injection Prevention
+
+- Always use parameterized queries — never concatenate user input into SQL
+- Use `PreparedStatement` or your framework's parameterized query API
+- Validate and sanitize any input used in native queries
+
+```java
+// BAD — SQL injection via string concatenation
+Statement stmt = conn.createStatement();
+String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
+stmt.executeQuery(sql);
+
+// GOOD — PreparedStatement with parameterized query
+PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
+ps.setString(1, name);
+
+// GOOD — JDBC template
+jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
+```
+
+## Input Validation
+
+- Validate all user input at system boundaries before processing
+- Use Bean Validation (`@NotNull`, `@NotBlank`, `@Size`) on DTOs when using a validation framework
+- Sanitize file paths and user-provided strings before use
+- Reject input that fails validation with clear error messages
+
+```java
+// Validate manually in plain Java
+public Order createOrder(String customerName, BigDecimal amount) {
+ if (customerName == null || customerName.isBlank()) {
+ throw new IllegalArgumentException("Customer name is required");
+ }
+ if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("Amount must be positive");
+ }
+ return new Order(customerName, amount);
+}
+```
+
+## Authentication and Authorization
+
+- Never implement custom auth crypto — use established libraries
+- Store passwords with bcrypt or Argon2, never MD5/SHA1
+- Enforce authorization checks at service boundaries
+- Clear sensitive data from logs — never log passwords, tokens, or PII
+
+## Dependency Security
+
+- Run `mvn dependency:tree` or `./gradlew dependencies` to audit transitive dependencies
+- Use OWASP Dependency-Check or Snyk to scan for known CVEs
+- Keep dependencies updated — set up Dependabot or Renovate
+
+## Error Messages
+
+- Never expose stack traces, internal paths, or SQL errors in API responses
+- Map exceptions to safe, generic client messages at handler boundaries
+- Log detailed errors server-side; return generic messages to clients
+
+```java
+// Log the detail, return a generic message
+try {
+ return orderService.findById(id);
+} catch (OrderNotFoundException ex) {
+ log.warn("Order not found: id={}", id);
+ return ApiResponse.error("Resource not found"); // generic, no internals
+} catch (Exception ex) {
+ log.error("Unexpected error processing order id={}", id, ex);
+ return ApiResponse.error("Internal server error"); // never expose ex.getMessage()
+}
+```
+
+## References
+
+See skill: `springboot-security` for Spring Security authentication and authorization patterns.
+See skill: `quarkus-security` for Quarkus security with JWT/OIDC, RBAC, and CDI.
+See skill: `security-review` for general security checklists.
diff --git a/.claude/rules/ecc/java/testing.md b/.claude/rules/ecc/java/testing.md
new file mode 100644
index 0000000..177b264
--- /dev/null
+++ b/.claude/rules/ecc/java/testing.md
@@ -0,0 +1,133 @@
+---
+paths:
+ - "**/*.java"
+---
+# Java Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Java-specific content.
+
+## Test Framework
+
+- **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`)
+- **AssertJ** for fluent assertions (`assertThat(result).isEqualTo(expected)`)
+- **Mockito** for mocking dependencies
+- **Testcontainers** for integration tests requiring databases or services
+
+## Test Organization
+
+```
+src/test/java/com/example/app/
+ service/ # Unit tests for service layer
+ controller/ # Web layer / API tests
+ repository/ # Data access tests
+ integration/ # Cross-layer integration tests
+```
+
+Mirror the `src/main/java` package structure in `src/test/java`.
+
+## Unit Test Pattern
+
+```java
+@ExtendWith(MockitoExtension.class)
+class OrderServiceTest {
+
+ @Mock
+ private OrderRepository orderRepository;
+
+ private OrderService orderService;
+
+ @BeforeEach
+ void setUp() {
+ orderService = new OrderService(orderRepository);
+ }
+
+ @Test
+ @DisplayName("findById returns order when exists")
+ void findById_existingOrder_returnsOrder() {
+ var order = new Order(1L, "Alice", BigDecimal.TEN);
+ when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
+
+ var result = orderService.findById(1L);
+
+ assertThat(result.customerName()).isEqualTo("Alice");
+ verify(orderRepository).findById(1L);
+ }
+
+ @Test
+ @DisplayName("findById throws when order not found")
+ void findById_missingOrder_throws() {
+ when(orderRepository.findById(99L)).thenReturn(Optional.empty());
+
+ assertThatThrownBy(() -> orderService.findById(99L))
+ .isInstanceOf(OrderNotFoundException.class)
+ .hasMessageContaining("99");
+ }
+}
+```
+
+## Parameterized Tests
+
+```java
+@ParameterizedTest
+@CsvSource({
+ "100.00, 10, 90.00",
+ "50.00, 0, 50.00",
+ "200.00, 25, 150.00"
+})
+@DisplayName("discount applied correctly")
+void applyDiscount(BigDecimal price, int pct, BigDecimal expected) {
+ assertThat(PricingUtils.discount(price, pct)).isEqualByComparingTo(expected);
+}
+```
+
+## Integration Tests
+
+Use Testcontainers for real database integration:
+
+```java
+@Testcontainers
+class OrderRepositoryIT {
+
+ @Container
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:16");
+
+ private OrderRepository repository;
+
+ @BeforeEach
+ void setUp() {
+ var dataSource = new PGSimpleDataSource();
+ dataSource.setUrl(postgres.getJdbcUrl());
+ dataSource.setUser(postgres.getUsername());
+ dataSource.setPassword(postgres.getPassword());
+ repository = new JdbcOrderRepository(dataSource);
+ }
+
+ @Test
+ void save_and_findById() {
+ var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
+ var found = repository.findById(saved.getId());
+ assertThat(found).isPresent();
+ }
+}
+```
+
+For Spring Boot integration tests, see skill: `springboot-tdd`.
+For Quarkus integration tests, see skill: `quarkus-tdd`.
+
+## Test Naming
+
+Use descriptive names with `@DisplayName`:
+- `methodName_scenario_expectedBehavior()` for method names
+- `@DisplayName("human-readable description")` for reports
+
+## Coverage
+
+- Target 80%+ line coverage
+- Use JaCoCo for coverage reporting
+- Focus on service and domain logic — skip trivial getters/config classes
+
+## References
+
+See skill: `springboot-tdd` for Spring Boot TDD patterns with MockMvc and Testcontainers.
+See skill: `quarkus-tdd` for Quarkus TDD patterns with REST Assured and Dev Services.
+See skill: `java-coding-standards` for testing expectations.
diff --git a/.claude/rules/ecc/kotlin/coding-style.md b/.claude/rules/ecc/kotlin/coding-style.md
new file mode 100644
index 0000000..5c5ee30
--- /dev/null
+++ b/.claude/rules/ecc/kotlin/coding-style.md
@@ -0,0 +1,86 @@
+---
+paths:
+ - "**/*.kt"
+ - "**/*.kts"
+---
+# Kotlin Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Kotlin-specific content.
+
+## Formatting
+
+- **ktlint** or **Detekt** for style enforcement
+- Official Kotlin code style (`kotlin.code.style=official` in `gradle.properties`)
+
+## Immutability
+
+- Prefer `val` over `var` — default to `val` and only use `var` when mutation is required
+- Use `data class` for value types; use immutable collections (`List`, `Map`, `Set`) in public APIs
+- Copy-on-write for state updates: `state.copy(field = newValue)`
+
+## Naming
+
+Follow Kotlin conventions:
+- `camelCase` for functions and properties
+- `PascalCase` for classes, interfaces, objects, and type aliases
+- `SCREAMING_SNAKE_CASE` for constants (`const val` or `@JvmStatic`)
+- Prefix interfaces with behavior, not `I`: `Clickable` not `IClickable`
+
+## Null Safety
+
+- Never use `!!` — prefer `?.`, `?:`, `requireNotNull()`, or `checkNotNull()`
+- Use `?.let {}` for scoped null-safe operations
+- Return nullable types from functions that can legitimately have no result
+
+```kotlin
+// BAD
+val name = user!!.name
+
+// GOOD
+val name = user?.name ?: "Unknown"
+val name = requireNotNull(user) { "User must be set before accessing name" }.name
+```
+
+## Sealed Types
+
+Use sealed classes/interfaces to model closed state hierarchies:
+
+```kotlin
+sealed interface UiState {
+ data object Loading : UiState
+ data class Success(val data: T) : UiState
+ data class Error(val message: String) : UiState
+}
+```
+
+Always use exhaustive `when` with sealed types — no `else` branch.
+
+## Extension Functions
+
+Use extension functions for utility operations, but keep them discoverable:
+- Place in a file named after the receiver type (`StringExt.kt`, `FlowExt.kt`)
+- Keep scope limited — don't add extensions to `Any` or overly generic types
+
+## Scope Functions
+
+Use the right scope function:
+- `let` — null check + transform: `user?.let { greet(it) }`
+- `run` — compute a result using receiver: `service.run { fetch(config) }`
+- `apply` — configure an object: `builder.apply { timeout = 30 }`
+- `also` — side effects: `result.also { log(it) }`
+- Avoid deep nesting of scope functions (max 2 levels)
+
+## Error Handling
+
+- Use `Result` or custom sealed types
+- Use `runCatching {}` for wrapping throwable code
+- Never catch `CancellationException` — always rethrow it
+- Avoid `try-catch` for control flow
+
+```kotlin
+// BAD — using exceptions for control flow
+val user = try { repository.getUser(id) } catch (e: NotFoundException) { null }
+
+// GOOD — nullable return
+val user: User? = repository.findUser(id)
+```
diff --git a/.claude/rules/ecc/kotlin/hooks.md b/.claude/rules/ecc/kotlin/hooks.md
new file mode 100644
index 0000000..28bb02f
--- /dev/null
+++ b/.claude/rules/ecc/kotlin/hooks.md
@@ -0,0 +1,17 @@
+---
+paths:
+ - "**/*.kt"
+ - "**/*.kts"
+ - "**/build.gradle.kts"
+---
+# Kotlin Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Kotlin-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **ktfmt/ktlint**: Auto-format `.kt` and `.kts` files after edit
+- **detekt**: Run static analysis after editing Kotlin files
+- **./gradlew build**: Verify compilation after changes
diff --git a/.claude/rules/ecc/kotlin/patterns.md b/.claude/rules/ecc/kotlin/patterns.md
new file mode 100644
index 0000000..1a09e6b
--- /dev/null
+++ b/.claude/rules/ecc/kotlin/patterns.md
@@ -0,0 +1,146 @@
+---
+paths:
+ - "**/*.kt"
+ - "**/*.kts"
+---
+# Kotlin Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Kotlin and Android/KMP-specific content.
+
+## Dependency Injection
+
+Prefer constructor injection. Use Koin (KMP) or Hilt (Android-only):
+
+```kotlin
+// Koin — declare modules
+val dataModule = module {
+ single { ItemRepositoryImpl(get(), get()) }
+ factory { GetItemsUseCase(get()) }
+ viewModelOf(::ItemListViewModel)
+}
+
+// Hilt — annotations
+@HiltViewModel
+class ItemListViewModel @Inject constructor(
+ private val getItems: GetItemsUseCase
+) : ViewModel()
+```
+
+## ViewModel Pattern
+
+Single state object, event sink, one-way data flow:
+
+```kotlin
+data class ScreenState(
+ val items: List = emptyList(),
+ val isLoading: Boolean = false
+)
+
+class ScreenViewModel(private val useCase: GetItemsUseCase) : ViewModel() {
+ private val _state = MutableStateFlow(ScreenState())
+ val state = _state.asStateFlow()
+
+ fun onEvent(event: ScreenEvent) {
+ when (event) {
+ is ScreenEvent.Load -> load()
+ is ScreenEvent.Delete -> delete(event.id)
+ }
+ }
+}
+```
+
+## Repository Pattern
+
+- `suspend` functions return `Result` or custom error type
+- `Flow` for reactive streams
+- Coordinate local + remote data sources
+
+```kotlin
+interface ItemRepository {
+ suspend fun getById(id: String): Result
+ suspend fun getAll(): Result>
+ fun observeAll(): Flow>
+}
+```
+
+## UseCase Pattern
+
+Single responsibility, `operator fun invoke`:
+
+```kotlin
+class GetItemUseCase(private val repository: ItemRepository) {
+ suspend operator fun invoke(id: String): Result {
+ return repository.getById(id)
+ }
+}
+
+class GetItemsUseCase(private val repository: ItemRepository) {
+ suspend operator fun invoke(): Result> {
+ return repository.getAll()
+ }
+}
+```
+
+## expect/actual (KMP)
+
+Use for platform-specific implementations:
+
+```kotlin
+// commonMain
+expect fun platformName(): String
+expect class SecureStorage {
+ fun save(key: String, value: String)
+ fun get(key: String): String?
+}
+
+// androidMain
+actual fun platformName(): String = "Android"
+actual class SecureStorage {
+ actual fun save(key: String, value: String) { /* EncryptedSharedPreferences */ }
+ actual fun get(key: String): String? = null /* ... */
+}
+
+// iosMain
+actual fun platformName(): String = "iOS"
+actual class SecureStorage {
+ actual fun save(key: String, value: String) { /* Keychain */ }
+ actual fun get(key: String): String? = null /* ... */
+}
+```
+
+## Coroutine Patterns
+
+- Use `viewModelScope` in ViewModels, `coroutineScope` for structured child work
+- Use `stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), initialValue)` for StateFlow from cold Flows
+- Use `supervisorScope` when child failures should be independent
+
+## Builder Pattern with DSL
+
+```kotlin
+class HttpClientConfig {
+ var baseUrl: String = ""
+ var timeout: Long = 30_000
+ private val interceptors = mutableListOf()
+
+ fun interceptor(block: () -> Interceptor) {
+ interceptors.add(block())
+ }
+}
+
+fun httpClient(block: HttpClientConfig.() -> Unit): HttpClient {
+ val config = HttpClientConfig().apply(block)
+ return HttpClient(config)
+}
+
+// Usage
+val client = httpClient {
+ baseUrl = "https://api.example.com"
+ timeout = 15_000
+ interceptor { AuthInterceptor(tokenProvider) }
+}
+```
+
+## References
+
+See skill: `kotlin-coroutines-flows` for detailed coroutine patterns.
+See skill: `android-clean-architecture` for module and layer patterns.
diff --git a/.claude/rules/ecc/kotlin/security.md b/.claude/rules/ecc/kotlin/security.md
new file mode 100644
index 0000000..a212211
--- /dev/null
+++ b/.claude/rules/ecc/kotlin/security.md
@@ -0,0 +1,82 @@
+---
+paths:
+ - "**/*.kt"
+ - "**/*.kts"
+---
+# Kotlin Security
+
+> This file extends [common/security.md](../common/security.md) with Kotlin and Android/KMP-specific content.
+
+## Secrets Management
+
+- Never hardcode API keys, tokens, or credentials in source code
+- Use `local.properties` (git-ignored) for local development secrets
+- Use `BuildConfig` fields generated from CI secrets for release builds
+- Use `EncryptedSharedPreferences` (Android) or Keychain (iOS) for runtime secret storage
+
+```kotlin
+// BAD
+val apiKey = "sk-abc123..."
+
+// GOOD — from BuildConfig (generated at build time)
+val apiKey = BuildConfig.API_KEY
+
+// GOOD — from secure storage at runtime
+val token = secureStorage.get("auth_token")
+```
+
+## Network Security
+
+- Use HTTPS exclusively — configure `network_security_config.xml` to block cleartext
+- Pin certificates for sensitive endpoints using OkHttp `CertificatePinner` or Ktor equivalent
+- Set timeouts on all HTTP clients — never leave defaults (which may be infinite)
+- Validate and sanitize all server responses before use
+
+```xml
+
+
+
+
+```
+
+## Input Validation
+
+- Validate all user input before processing or sending to API
+- Use parameterized queries for Room/SQLDelight — never concatenate user input into SQL
+- Sanitize file paths from user input to prevent path traversal
+
+```kotlin
+// BAD — SQL injection
+@Query("SELECT * FROM items WHERE name = '$input'")
+
+// GOOD — parameterized
+@Query("SELECT * FROM items WHERE name = :input")
+fun findByName(input: String): List
+```
+
+## Data Protection
+
+- Use `EncryptedSharedPreferences` for sensitive key-value data on Android
+- Use `@Serializable` with explicit field names — don't leak internal property names
+- Clear sensitive data from memory when no longer needed
+- Use `@Keep` or ProGuard rules for serialized classes to prevent name mangling
+
+## Authentication
+
+- Store tokens in secure storage, not in plain SharedPreferences
+- Implement token refresh with proper 401/403 handling
+- Clear all auth state on logout (tokens, cached user data, cookies)
+- Use biometric authentication (`BiometricPrompt`) for sensitive operations
+
+## ProGuard / R8
+
+- Keep rules for all serialized models (`@Serializable`, Gson, Moshi)
+- Keep rules for reflection-based libraries (Koin, Retrofit)
+- Test release builds — obfuscation can break serialization silently
+
+## WebView Security
+
+- Disable JavaScript unless explicitly needed: `settings.javaScriptEnabled = false`
+- Validate URLs before loading in WebView
+- Never expose `@JavascriptInterface` methods that access sensitive data
+- Use `WebViewClient.shouldOverrideUrlLoading()` to control navigation
diff --git a/.claude/rules/ecc/kotlin/testing.md b/.claude/rules/ecc/kotlin/testing.md
new file mode 100644
index 0000000..cdf9733
--- /dev/null
+++ b/.claude/rules/ecc/kotlin/testing.md
@@ -0,0 +1,128 @@
+---
+paths:
+ - "**/*.kt"
+ - "**/*.kts"
+---
+# Kotlin Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Kotlin and Android/KMP-specific content.
+
+## Test Framework
+
+- **kotlin.test** for multiplatform (KMP) — `@Test`, `assertEquals`, `assertTrue`
+- **JUnit 4/5** for Android-specific tests
+- **Turbine** for testing Flows and StateFlow
+- **kotlinx-coroutines-test** for coroutine testing (`runTest`, `TestDispatcher`)
+
+## ViewModel Testing with Turbine
+
+```kotlin
+@Test
+fun `loading state emitted then data`() = runTest {
+ val repo = FakeItemRepository()
+ repo.addItem(testItem)
+ val viewModel = ItemListViewModel(GetItemsUseCase(repo))
+
+ viewModel.state.test {
+ assertEquals(ItemListState(), awaitItem()) // initial state
+ viewModel.onEvent(ItemListEvent.Load)
+ assertTrue(awaitItem().isLoading) // loading
+ assertEquals(listOf(testItem), awaitItem().items) // loaded
+ }
+}
+```
+
+## Fakes Over Mocks
+
+Prefer hand-written fakes over mocking frameworks:
+
+```kotlin
+class FakeItemRepository : ItemRepository {
+ private val items = mutableListOf()
+ var fetchError: Throwable? = null
+
+ override suspend fun getAll(): Result> {
+ fetchError?.let { return Result.failure(it) }
+ return Result.success(items.toList())
+ }
+
+ override fun observeAll(): Flow> = flowOf(items.toList())
+
+ fun addItem(item: Item) { items.add(item) }
+}
+```
+
+## Coroutine Testing
+
+```kotlin
+@Test
+fun `parallel operations complete`() = runTest {
+ val repo = FakeRepository()
+ val result = loadDashboard(repo)
+ advanceUntilIdle()
+ assertNotNull(result.items)
+ assertNotNull(result.stats)
+}
+```
+
+Use `runTest` — it auto-advances virtual time and provides `TestScope`.
+
+## Ktor MockEngine
+
+```kotlin
+val mockEngine = MockEngine { request ->
+ when (request.url.encodedPath) {
+ "/api/items" -> respond(
+ content = Json.encodeToString(testItems),
+ headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
+ )
+ else -> respondError(HttpStatusCode.NotFound)
+ }
+}
+
+val client = HttpClient(mockEngine) {
+ install(ContentNegotiation) { json() }
+}
+```
+
+## Room/SQLDelight Testing
+
+- Room: Use `Room.inMemoryDatabaseBuilder()` for in-memory testing
+- SQLDelight: Use `JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)` for JVM tests
+
+```kotlin
+@Test
+fun `insert and query items`() = runTest {
+ val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
+ Database.Schema.create(driver)
+ val db = Database(driver)
+
+ db.itemQueries.insert("1", "Sample Item", "description")
+ val items = db.itemQueries.getAll().executeAsList()
+ assertEquals(1, items.size)
+}
+```
+
+## Test Naming
+
+Use backtick-quoted descriptive names:
+
+```kotlin
+@Test
+fun `search with empty query returns all items`() = runTest { }
+
+@Test
+fun `delete item emits updated list without deleted item`() = runTest { }
+```
+
+## Test Organization
+
+```
+src/
+├── commonTest/kotlin/ # Shared tests (ViewModel, UseCase, Repository)
+├── androidUnitTest/kotlin/ # Android unit tests (JUnit)
+├── androidInstrumentedTest/kotlin/ # Instrumented tests (Room, UI)
+└── iosTest/kotlin/ # iOS-specific tests
+```
+
+Minimum test coverage: ViewModel + UseCase for every feature.
diff --git a/.claude/rules/ecc/nuxt/coding-style.md b/.claude/rules/ecc/nuxt/coding-style.md
new file mode 100644
index 0000000..b8a938a
--- /dev/null
+++ b/.claude/rules/ecc/nuxt/coding-style.md
@@ -0,0 +1,47 @@
+---
+paths:
+ - "**/nuxt.config.*"
+ - "**/app.config.*"
+ - "**/app.vue"
+ - "**/pages/**"
+ - "**/layouts/**"
+ - "**/middleware/**"
+---
+
+# Nuxt Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Nuxt specific content.
+
+## Directory layout
+
+- Default `srcDir` is `app/`. Framework files live at `app/pages/`, `app/layouts/`, `app/middleware/`, `app/plugins/`, `app/app.config.ts`. `nuxt.config.ts` and `server/` stay at project root.
+- Some projects override `srcDir` to `src/` for a Feature-Sliced Design layout, remapping `dir.pages` (for example to `src/app/routes`), `dir.layouts`, and the `@`/`~` aliases. Always check `nuxt.config.ts` before assuming a path.
+
+## Auto-imports discipline
+
+- Composables in `app/composables/` and `server/utils/` auto-import. Do NOT manually import Nuxt composables (`useFetch`, `useState`, `navigateTo`) or `defineStore` / `storeToRefs`.
+- Do NOT add a standalone `vue-router` dep (Nuxt bundles v5) or hand-mount `createApp` / `createPinia` / `createRouter`. The framework wires these.
+
+## Compiler macros
+
+- `definePageMeta` is a compile-time macro. Static values only, no reactive data and no side-effect calls inside it.
+- Augment typed `PageMeta` via `declare module '#app'` rather than casting.
+
+## Config file separation
+
+Three distinct files, do not conflate.
+
+- `nuxt.config.ts` = build-time only (`routeRules`, `modules`, `nitro`, `ssr` flag). Not reactive.
+- `runtimeConfig` (inside nuxt.config) = per-env runtime values, env-overridable via `NUXT_*`. Root keys are server-only, `public` keys are client-visible.
+- `app/app.config.ts` = public build-fixed reactive settings (theme tokens, feature flags). No env override. NEVER secrets.
+
+## Head and meta
+
+- `app.head` in `nuxt.config.ts` takes static values only.
+- Reactive meta goes through `useHead` / `useSeoMeta` in component setup, never via `app.head`.
+
+## Reference
+
+- ECC skills: `nuxt4-patterns`, `vite-patterns`, `frontend-patterns`.
+- [Nuxt directory structure](https://nuxt.com/docs/guide/directory-structure/app)
+- [Nuxt configuration](https://nuxt.com/docs/api/nuxt-config)
diff --git a/.claude/rules/ecc/nuxt/hooks.md b/.claude/rules/ecc/nuxt/hooks.md
new file mode 100644
index 0000000..a561c9d
--- /dev/null
+++ b/.claude/rules/ecc/nuxt/hooks.md
@@ -0,0 +1,39 @@
+---
+paths:
+ - "**/nuxt.config.*"
+ - "**/app.config.*"
+ - "**/server/**/*.ts"
+ - "**/*.vue"
+---
+
+# Nuxt Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Nuxt specific content.
+
+These are Claude Code harness hooks for Nuxt work. They run via the harness, not Claude.
+
+## Typecheck
+
+- `nuxi typecheck` wraps `vue-tsc`. Requires `vue-tsc` + `typescript` dev deps.
+- Run on `.vue` / `.ts` edit or pre-commit. Typecheck is project-wide, so debounce it and wrap it in a timeout (mirror `web/hooks.md`, for example `timeout 60 nuxi typecheck`) so a hung type-check is reaped instead of accumulating across fast edits.
+
+## Lint
+
+- Use the `@nuxt/eslint` module (flat-config, project-aware, generates `.nuxt/eslint.config.mjs`).
+- Run `eslint .` or `eslint --fix`. This is the Nuxt-official ESLint integration, prefer it over hand-rolled configs.
+
+## Format
+
+- `prettier --write`, or enable stylistic rules in `@nuxt/eslint` to avoid a Prettier/ESLint conflict.
+- Pick one formatting authority. Do not run both Prettier and ESLint stylistic at once.
+
+## Suggested PostToolUse chain
+
+- On Edit to `app/**` and `server/**`: run `eslint --fix` then `timeout 60 nuxi typecheck`.
+- Order matters: lint-fix first (mutates the file), the timed typecheck second (verifies the result). Debouncing still applies.
+
+## Reference
+
+- ECC skills: `nuxt4-patterns`, `vite-patterns`.
+- [@nuxt/eslint module](https://eslint.nuxt.com/)
+- [nuxi typecheck](https://nuxt.com/docs/api/commands/typecheck)
diff --git a/.claude/rules/ecc/nuxt/patterns.md b/.claude/rules/ecc/nuxt/patterns.md
new file mode 100644
index 0000000..cb41be4
--- /dev/null
+++ b/.claude/rules/ecc/nuxt/patterns.md
@@ -0,0 +1,54 @@
+---
+paths:
+ - "**/nuxt.config.*"
+ - "**/app.config.*"
+ - "**/app.vue"
+ - "**/server/**/*.ts"
+ - "**/pages/**"
+ - "**/middleware/**"
+---
+
+# Nuxt Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Nuxt specific content.
+
+## Data-fetch selection
+
+Load-bearing. Pick by render timing, not habit.
+
+- `useFetch(url)` = SSR-safe, URL-first initial/first-paint data. The default. Forwards the server result through the payload so there is no hydration double-fetch.
+- `useAsyncData(key, fn)` = SSR-safe, custom async logic (SDK / GraphQL / combined calls). The explicit key shares the result across components.
+- `$fetch` = client interactions only (form submit, button click, POST/PUT/DELETE). NOT SSR-safe, double-fetches if used for first paint.
+- Rule: `useFetch` / `useAsyncData` for anything rendered on first paint, `$fetch` only for event-driven mutations.
+
+## Shared state
+
+- `useState('key', () => init)` for SSR-safe shared state. Values must be JSON-serializable.
+- NEVER `export const x = ref()` at module scope. One shared instance leaks across concurrent SSR requests and causes a memory leak.
+- With `@pinia/nuxt`: Pinia for domain state, `useState` for small cross-component primitives.
+- Async server-side init goes in `callOnce(async () => {...})`, not as a side effect inside `useAsyncData`.
+
+## Nitro server routes
+
+- `server/api/*.{get,post}.ts` auto-register by path + method. Handler is `defineEventHandler((event) => ...)`.
+- Errors via `throw createError({ status, statusText })`. Prefer the Web-API `status` / `statusText` over deprecated `statusCode` / `statusMessage`.
+- `server/middleware/` must NOT return a response. Only mutate `event.context` or set headers.
+
+## Route middleware
+
+- `app/middleware/*.ts` with `defineNuxtRouteMiddleware((to, from) => ...)`.
+- Use the `to` / `from` args. Do NOT call `useRoute()` inside middleware.
+- `.global` suffix runs on every route. Return `navigateTo()` to redirect, `abortNavigation()` to stop.
+
+## Hydration-safe rendering
+
+- Route off `status` (`idle | pending | success | error`) for lazy fetches.
+- `useAsyncData` payload uses `devalue` (Date/Map/Set/refs survive). A `server/api` response is `JSON.stringify`-only, so define `toJSON()` for non-JSON types.
+- Shrink payload with `pick` / `transform`. This reduces serialized size, it does not skip the fetch.
+
+## Reference
+
+- ECC skills: `nuxt4-patterns`, `vite-patterns`, `frontend-patterns`.
+- [Nuxt data fetching](https://nuxt.com/docs/getting-started/data-fetching)
+- [Nuxt state management](https://nuxt.com/docs/getting-started/state-management)
+- [Nuxt server engine (Nitro)](https://nuxt.com/docs/guide/directory-structure/server)
diff --git a/.claude/rules/ecc/nuxt/security.md b/.claude/rules/ecc/nuxt/security.md
new file mode 100644
index 0000000..37f45bb
--- /dev/null
+++ b/.claude/rules/ecc/nuxt/security.md
@@ -0,0 +1,48 @@
+---
+paths:
+ - "**/nuxt.config.*"
+ - "**/app.config.*"
+ - "**/server/**/*.ts"
+---
+
+# Nuxt Security
+
+> This file extends [common/security.md](../common/security.md) with Nuxt specific content.
+
+## runtimeConfig public vs private
+
+- Root `runtimeConfig` keys are server-only. `runtimeConfig.public` serializes into EVERY page payload (client-visible).
+- Secrets go at root only. Never put secrets in `app.config.ts` or `runtimeConfig.public`, both ship to the client bundle.
+- Official warning: "Be careful not to expose runtime config keys to the client-side by either rendering them or passing them to `useState`."
+
+## Server-route input validation
+
+- Use h3 validating readers. Do NOT trust raw `readBody` / `getQuery` / `getRouterParam`.
+ - `readValidatedBody(event, schema)` validates the body.
+ - `getValidatedQuery(event, schema)` validates the query.
+ - `getValidatedRouterParams(event, schema)` validates route params.
+- All accept a validation function or a Zod schema and throw on failure.
+
+## SSR payload leakage
+
+- Anything in `useState`, `useFetch` / `useAsyncData` results, or `runtimeConfig.public` is serialized into the client payload. Never write a secret into those.
+- Use `useServerSeoMeta` for server-only meta with no client cost.
+
+## Cookie and auth passthrough on SSR
+
+- Nuxt does NOT auto-attach the incoming user's cookies to outbound server-side `$fetch`.
+- Forward explicitly with `useRequestFetch()` (cleanest, pre-bound to request headers) or `useRequestHeaders(['cookie'])`.
+- Relay a backend `Set-Cookie` to the browser via `$fetch.raw` + `appendResponseHeader(event, 'set-cookie', ...)`.
+- socket.io is client-only (`.client.ts` plugin), never SSR.
+
+## SSRF on server $fetch
+
+- Server routes run with full network egress. Never pass user-controlled input directly into a server-side `$fetch` URL or host.
+- Validate the param first (h3 utilities above), allowlist the target, pin to `runtimeConfig.public.apiBase`, reject user-supplied absolute URLs.
+- Auto-trigger `/security-review` only for routes that make external network requests (server `$fetch`), handle auth tokens or credentials, or perform sensitive mutations or authorization checks. Examples: SSRF-prone proxy endpoints, token exchange or password reset, admin actions. Skip benign read-only routes that only accept validated query params.
+
+## Reference
+
+- ECC skills: `security-review`, `nuxt4-patterns`.
+- [Nuxt runtime config](https://nuxt.com/docs/guide/going-further/runtime-config)
+- [h3 request utils](https://v1.h3.dev/utils/request)
diff --git a/.claude/rules/ecc/nuxt/testing.md b/.claude/rules/ecc/nuxt/testing.md
new file mode 100644
index 0000000..ac3744f
--- /dev/null
+++ b/.claude/rules/ecc/nuxt/testing.md
@@ -0,0 +1,49 @@
+---
+paths:
+ - "**/nuxt.config.*"
+ - "**/server/**/*.ts"
+ - "**/pages/**"
+ - "**/layouts/**"
+ - "**/middleware/**"
+---
+
+# Nuxt Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Nuxt specific content.
+
+Package: `@nuxt/test-utils`. Vitest-first for unit and component tests, with built-in Playwright browser E2E support. nuxt-vitest and vitest-environment-nuxt are superseded and folded into it.
+
+## Setup
+
+- Install dev deps: `@nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core`.
+- Config: `defineVitestConfig({ test: { environment: 'nuxt' } })` from `@nuxt/test-utils/config`. Use `defineVitestProject` for multi-project (separate unit / nuxt / e2e environments).
+- Add `@nuxt/test-utils/module` to `nuxt.config`. Per-file opt-in via `// @vitest-environment nuxt`.
+
+## Runtime helpers
+
+Import from `@nuxt/test-utils/runtime`.
+
+- `mountSuspended(component, opts)` mounts in the Nuxt env with async setup + plugin injection (accepts `@vue/test-utils` mount options + `route`).
+- `renderSuspended(component, opts)` is the Testing Library variant (needs `@testing-library/vue`).
+- `mockNuxtImport(name, factory)` mocks auto-imports (e.g. `useState`). Once per import per file, use `vi.hoisted()`.
+- `mockComponent(name, factory)` mocks by PascalCase name or path.
+- `registerEndpoint(path, handler|opts)` mocks a Nitro endpoint to test server routes or stub the backend. Supports method + `once`.
+
+## E2E helpers
+
+Import from `@nuxt/test-utils/e2e`.
+
+- `await setup({ rootDir, server, browser, ... })` inside the describe block (manages beforeAll/afterAll).
+- Then `$fetch(url)` (rendered HTML), `fetch(url)` (response object), `url(path)` (full URL with port), `createPage(url)` (Playwright).
+- Playwright integration: import `expect` / `test` from `@nuxt/test-utils/playwright`.
+
+## What to test how
+
+- Composables: mock auto-imports with `mockNuxtImport`, mount a host component via `mountSuspended` to exercise `useState` / `useFetch` in the Nuxt runtime.
+- Server routes: `registerEndpoint` to stub, or e2e `$fetch` / `fetch` against the real Nitro server.
+
+## Reference
+
+- ECC skills: `nuxt4-patterns`, `e2e-testing`, `vite-patterns`.
+- [Nuxt testing docs](https://nuxt.com/docs/getting-started/testing)
+- [@nuxt/test-utils npm](https://www.npmjs.com/package/@nuxt/test-utils)
diff --git a/.claude/rules/ecc/perl/coding-style.md b/.claude/rules/ecc/perl/coding-style.md
new file mode 100644
index 0000000..9c7fbb2
--- /dev/null
+++ b/.claude/rules/ecc/perl/coding-style.md
@@ -0,0 +1,46 @@
+---
+paths:
+ - "**/*.pl"
+ - "**/*.pm"
+ - "**/*.t"
+ - "**/*.psgi"
+ - "**/*.cgi"
+---
+# Perl Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Perl-specific content.
+
+## Standards
+
+- Always `use v5.36` (enables `strict`, `warnings`, `say`, subroutine signatures)
+- Use subroutine signatures — never unpack `@_` manually
+- Prefer `say` over `print` with explicit newlines
+
+## Immutability
+
+- Use **Moo** with `is => 'ro'` and `Types::Standard` for all attributes
+- Never use blessed hashrefs directly — always use Moo/Moose accessors
+- **OO override note**: Moo `has` attributes with `builder` or `default` are acceptable for computed read-only values
+
+## Formatting
+
+Use **perltidy** with these settings:
+
+```
+-i=4 # 4-space indent
+-l=100 # 100 char line length
+-ce # cuddled else
+-bar # opening brace always right
+```
+
+## Linting
+
+Use **perlcritic** at severity 3 with themes: `core`, `pbp`, `security`.
+
+```bash
+perlcritic --severity 3 --theme 'core || pbp || security' lib/
+```
+
+## Reference
+
+See skill: `perl-patterns` for comprehensive modern Perl idioms and best practices.
diff --git a/.claude/rules/ecc/perl/hooks.md b/.claude/rules/ecc/perl/hooks.md
new file mode 100644
index 0000000..0b6daad
--- /dev/null
+++ b/.claude/rules/ecc/perl/hooks.md
@@ -0,0 +1,22 @@
+---
+paths:
+ - "**/*.pl"
+ - "**/*.pm"
+ - "**/*.t"
+ - "**/*.psgi"
+ - "**/*.cgi"
+---
+# Perl Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Perl-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **perltidy**: Auto-format `.pl` and `.pm` files after edit
+- **perlcritic**: Run lint check after editing `.pm` files
+
+## Warnings
+
+- Warn about `print` in non-script `.pm` files — use `say` or a logging module (e.g., `Log::Any`)
diff --git a/.claude/rules/ecc/perl/patterns.md b/.claude/rules/ecc/perl/patterns.md
new file mode 100644
index 0000000..a2f7b4f
--- /dev/null
+++ b/.claude/rules/ecc/perl/patterns.md
@@ -0,0 +1,76 @@
+---
+paths:
+ - "**/*.pl"
+ - "**/*.pm"
+ - "**/*.t"
+ - "**/*.psgi"
+ - "**/*.cgi"
+---
+# Perl Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Perl-specific content.
+
+## Repository Pattern
+
+Use **DBI** or **DBIx::Class** behind an interface:
+
+```perl
+package MyApp::Repo::User;
+use Moo;
+
+has dbh => (is => 'ro', required => 1);
+
+sub find_by_id ($self, $id) {
+ my $sth = $self->dbh->prepare('SELECT * FROM users WHERE id = ?');
+ $sth->execute($id);
+ return $sth->fetchrow_hashref;
+}
+```
+
+## DTOs / Value Objects
+
+Use **Moo** classes with **Types::Standard** (equivalent to Python dataclasses):
+
+```perl
+package MyApp::DTO::User;
+use Moo;
+use Types::Standard qw(Str Int);
+
+has name => (is => 'ro', isa => Str, required => 1);
+has email => (is => 'ro', isa => Str, required => 1);
+has age => (is => 'ro', isa => Int);
+```
+
+## Resource Management
+
+- Always use **three-arg open** with `autodie`
+- Use **Path::Tiny** for file operations
+
+```perl
+use autodie;
+use Path::Tiny;
+
+my $content = path('config.json')->slurp_utf8;
+```
+
+## Module Interface
+
+Use `Exporter 'import'` with `@EXPORT_OK` — never `@EXPORT`:
+
+```perl
+use Exporter 'import';
+our @EXPORT_OK = qw(parse_config validate_input);
+```
+
+## Dependency Management
+
+Use **cpanfile** + **carton** for reproducible installs:
+
+```bash
+carton install
+carton exec prove -lr t/
+```
+
+## Reference
+
+See skill: `perl-patterns` for comprehensive modern Perl patterns and idioms.
diff --git a/.claude/rules/ecc/perl/security.md b/.claude/rules/ecc/perl/security.md
new file mode 100644
index 0000000..c87fefc
--- /dev/null
+++ b/.claude/rules/ecc/perl/security.md
@@ -0,0 +1,69 @@
+---
+paths:
+ - "**/*.pl"
+ - "**/*.pm"
+ - "**/*.t"
+ - "**/*.psgi"
+ - "**/*.cgi"
+---
+# Perl Security
+
+> This file extends [common/security.md](../common/security.md) with Perl-specific content.
+
+## Taint Mode
+
+- Use `-T` flag on all CGI/web-facing scripts
+- Sanitize `%ENV` (`$ENV{PATH}`, `$ENV{CDPATH}`, etc.) before any external command
+
+## Input Validation
+
+- Use allowlist regex for untainting — never `/(.*)/s`
+- Validate all user input with explicit patterns:
+
+```perl
+if ($input =~ /\A([a-zA-Z0-9_-]+)\z/) {
+ my $clean = $1;
+}
+```
+
+## File I/O
+
+- **Three-arg open only** — never two-arg open
+- Prevent path traversal with `Cwd::realpath`:
+
+```perl
+use Cwd 'realpath';
+my $safe_path = realpath($user_path);
+die "Path traversal" unless $safe_path =~ m{\A/allowed/directory/};
+```
+
+## Process Execution
+
+- Use **list-form `system()`** — never single-string form
+- Use **IPC::Run3** for capturing output
+- Never use backticks with variable interpolation
+
+```perl
+system('grep', '-r', $pattern, $directory); # safe
+```
+
+## SQL Injection Prevention
+
+Always use DBI placeholders — never interpolate into SQL:
+
+```perl
+my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
+$sth->execute($email);
+```
+
+## Security Scanning
+
+Run **perlcritic** with the security theme at severity 4+:
+
+```bash
+perlcritic --severity 4 --theme security lib/
+```
+
+## Reference
+
+See skill: `perl-security` for comprehensive Perl security patterns, taint mode, and safe I/O.
diff --git a/.claude/rules/ecc/perl/testing.md b/.claude/rules/ecc/perl/testing.md
new file mode 100644
index 0000000..d451699
--- /dev/null
+++ b/.claude/rules/ecc/perl/testing.md
@@ -0,0 +1,54 @@
+---
+paths:
+ - "**/*.pl"
+ - "**/*.pm"
+ - "**/*.t"
+ - "**/*.psgi"
+ - "**/*.cgi"
+---
+# Perl Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Perl-specific content.
+
+## Framework
+
+Use **Test2::V0** for new projects (not Test::More):
+
+```perl
+use Test2::V0;
+
+is($result, 42, 'answer is correct');
+
+done_testing;
+```
+
+## Runner
+
+```bash
+prove -l t/ # adds lib/ to @INC
+prove -lr -j8 t/ # recursive, 8 parallel jobs
+```
+
+Always use `-l` to ensure `lib/` is on `@INC`.
+
+## Coverage
+
+Use **Devel::Cover** — target 80%+:
+
+```bash
+cover -test
+```
+
+## Mocking
+
+- **Test::MockModule** — mock methods on existing modules
+- **Test::MockObject** — create test doubles from scratch
+
+## Pitfalls
+
+- Always end test files with `done_testing`
+- Never forget the `-l` flag with `prove`
+
+## Reference
+
+See skill: `perl-testing` for detailed Perl TDD patterns with Test2::V0, prove, and Devel::Cover.
diff --git a/.claude/rules/ecc/php/coding-style.md b/.claude/rules/ecc/php/coding-style.md
new file mode 100644
index 0000000..382ea9b
--- /dev/null
+++ b/.claude/rules/ecc/php/coding-style.md
@@ -0,0 +1,40 @@
+---
+paths:
+ - "**/*.php"
+ - "**/composer.json"
+---
+# PHP Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with PHP specific content.
+
+## Standards
+
+- Follow **PSR-12** formatting and naming conventions.
+- Prefer `declare(strict_types=1);` in application code.
+- Use scalar type hints, return types, and typed properties everywhere new code permits.
+
+## Immutability
+
+- Prefer immutable DTOs and value objects for data crossing service boundaries.
+- Use `readonly` properties or immutable constructors for request/response payloads where possible.
+- Keep arrays for simple maps; promote business-critical structures into explicit classes.
+
+## Formatting
+
+- Use **PHP-CS-Fixer** or **Laravel Pint** for formatting.
+- Use **PHPStan** or **Psalm** for static analysis.
+- Keep Composer scripts checked in so the same commands run locally and in CI.
+
+## Imports
+
+- Add `use` statements for all referenced classes, interfaces, and traits.
+- Avoid relying on the global namespace unless the project explicitly prefers fully qualified names.
+
+## Error Handling
+
+- Throw exceptions for exceptional states; avoid returning `false`/`null` as hidden error channels in new code.
+- Convert framework/request input into validated DTOs before it reaches domain logic.
+
+## Reference
+
+See skill: `backend-patterns` for broader service/repository layering guidance.
diff --git a/.claude/rules/ecc/php/hooks.md b/.claude/rules/ecc/php/hooks.md
new file mode 100644
index 0000000..10dd3c9
--- /dev/null
+++ b/.claude/rules/ecc/php/hooks.md
@@ -0,0 +1,24 @@
+---
+paths:
+ - "**/*.php"
+ - "**/composer.json"
+ - "**/phpstan.neon"
+ - "**/phpstan.neon.dist"
+ - "**/psalm.xml"
+---
+# PHP Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with PHP specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **Pint / PHP-CS-Fixer**: Auto-format edited `.php` files.
+- **PHPStan / Psalm**: Run static analysis after PHP edits in typed codebases.
+- **PHPUnit / Pest**: Run targeted tests for touched files or modules when edits affect behavior.
+
+## Warnings
+
+- Warn on `var_dump`, `dd`, `dump`, or `die()` left in edited files.
+- Warn when edited PHP files add raw SQL or disable CSRF/session protections.
diff --git a/.claude/rules/ecc/php/patterns.md b/.claude/rules/ecc/php/patterns.md
new file mode 100644
index 0000000..b914474
--- /dev/null
+++ b/.claude/rules/ecc/php/patterns.md
@@ -0,0 +1,33 @@
+---
+paths:
+ - "**/*.php"
+ - "**/composer.json"
+---
+# PHP Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with PHP specific content.
+
+## Thin Controllers, Explicit Services
+
+- Keep controllers focused on transport: auth, validation, serialization, status codes.
+- Move business rules into application/domain services that are easy to test without HTTP bootstrapping.
+
+## DTOs and Value Objects
+
+- Replace shape-heavy associative arrays with DTOs for requests, commands, and external API payloads.
+- Use value objects for money, identifiers, date ranges, and other constrained concepts.
+
+## Dependency Injection
+
+- Depend on interfaces or narrow service contracts, not framework globals.
+- Pass collaborators through constructors so services are testable without service-locator lookups.
+
+## Boundaries
+
+- Isolate ORM models from domain decisions when the model layer is doing more than persistence.
+- Wrap third-party SDKs behind small adapters so the rest of the codebase depends on your contract, not theirs.
+
+## Reference
+
+See skill: `api-design` for endpoint conventions and response-shape guidance.
+See skill: `laravel-patterns` for Laravel-specific architecture guidance.
diff --git a/.claude/rules/ecc/php/security.md b/.claude/rules/ecc/php/security.md
new file mode 100644
index 0000000..d559b8a
--- /dev/null
+++ b/.claude/rules/ecc/php/security.md
@@ -0,0 +1,37 @@
+---
+paths:
+ - "**/*.php"
+ - "**/composer.lock"
+ - "**/composer.json"
+---
+# PHP Security
+
+> This file extends [common/security.md](../common/security.md) with PHP specific content.
+
+## Input and Output
+
+- Validate request input at the framework boundary (`FormRequest`, Symfony Validator, or explicit DTO validation).
+- Escape output in templates by default; treat raw HTML rendering as an exception that must be justified.
+- Never trust query params, cookies, headers, or uploaded file metadata without validation.
+
+## Database Safety
+
+- Use prepared statements (`PDO`, Doctrine, Eloquent query builder) for all dynamic queries.
+- Avoid string-building SQL in controllers/views.
+- Scope ORM mass-assignment carefully and whitelist writable fields.
+
+## Secrets and Dependencies
+
+- Load secrets from environment variables or a secret manager, never from committed config files.
+- Run `composer audit` in CI and review new package maintainer trust before adding dependencies.
+- Pin major versions deliberately and remove abandoned packages quickly.
+
+## Auth and Session Safety
+
+- Use `password_hash()` / `password_verify()` for password storage.
+- Regenerate session identifiers after authentication and privilege changes.
+- Enforce CSRF protection on state-changing web requests.
+
+## Reference
+
+See skill: `laravel-security` for Laravel-specific security guidance.
diff --git a/.claude/rules/ecc/php/testing.md b/.claude/rules/ecc/php/testing.md
new file mode 100644
index 0000000..b069901
--- /dev/null
+++ b/.claude/rules/ecc/php/testing.md
@@ -0,0 +1,39 @@
+---
+paths:
+ - "**/*.php"
+ - "**/phpunit.xml"
+ - "**/phpunit.xml.dist"
+ - "**/composer.json"
+---
+# PHP Testing
+
+> This file extends [common/testing.md](../common/testing.md) with PHP specific content.
+
+## Framework
+
+Use **PHPUnit** as the default test framework. If **Pest** is configured in the project, prefer Pest for new tests and avoid mixing frameworks.
+
+## Coverage
+
+```bash
+vendor/bin/phpunit --coverage-text
+# or
+vendor/bin/pest --coverage
+```
+
+Prefer **pcov** or **Xdebug** in CI, and keep coverage thresholds in CI rather than as tribal knowledge.
+
+## Test Organization
+
+- Separate fast unit tests from framework/database integration tests.
+- Use factory/builders for fixtures instead of large hand-written arrays.
+- Keep HTTP/controller tests focused on transport and validation; move business rules into service-level tests.
+
+## Inertia
+
+If the project uses Inertia.js, prefer `assertInertia` with `AssertableInertia` to verify component names and props instead of raw JSON assertions.
+
+## Reference
+
+See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
+See skill: `laravel-tdd` for Laravel-specific testing patterns (PHPUnit and Pest).
diff --git a/.claude/rules/ecc/python/coding-style.md b/.claude/rules/ecc/python/coding-style.md
new file mode 100644
index 0000000..3a01ae3
--- /dev/null
+++ b/.claude/rules/ecc/python/coding-style.md
@@ -0,0 +1,42 @@
+---
+paths:
+ - "**/*.py"
+ - "**/*.pyi"
+---
+# Python Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Python specific content.
+
+## Standards
+
+- Follow **PEP 8** conventions
+- Use **type annotations** on all function signatures
+
+## Immutability
+
+Prefer immutable data structures:
+
+```python
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class User:
+ name: str
+ email: str
+
+from typing import NamedTuple
+
+class Point(NamedTuple):
+ x: float
+ y: float
+```
+
+## Formatting
+
+- **black** for code formatting
+- **isort** for import sorting
+- **ruff** for linting
+
+## Reference
+
+See skill: `python-patterns` for comprehensive Python idioms and patterns.
diff --git a/.claude/rules/ecc/python/fastapi.md b/.claude/rules/ecc/python/fastapi.md
new file mode 100644
index 0000000..6417b3a
--- /dev/null
+++ b/.claude/rules/ecc/python/fastapi.md
@@ -0,0 +1,58 @@
+---
+paths:
+ - "**/app/**/*.py"
+ - "**/fastapi/**/*.py"
+ - "**/*_api.py"
+---
+# FastAPI Rules
+
+Use these rules for FastAPI projects alongside the general Python rules.
+
+## Structure
+
+- Put app construction in `create_app()`.
+- Keep routers thin; move persistence and business behavior into services or CRUD helpers.
+- Keep request schemas, update schemas, and response schemas separate.
+- Keep database sessions and auth in dependencies.
+
+## Async
+
+- Use `async def` for endpoints that perform I/O.
+- Use async database and HTTP clients from async endpoints.
+- Do not call `requests`, sync SQLAlchemy sessions, or blocking file/network operations from async routes.
+
+## Dependency Injection
+
+```python
+@router.get("/users/{user_id}")
+async def get_user(
+ user_id: str,
+ db: AsyncSession = Depends(get_db),
+ current_user: User = Depends(get_current_user),
+):
+ ...
+```
+
+Do not create `SessionLocal()` or long-lived clients inside route handlers.
+
+## Schemas
+
+- Never include passwords, password hashes, access tokens, refresh tokens, or internal auth state in response models.
+- Use `response_model` on endpoints that return application data.
+- Use field constraints instead of hand-written validation when Pydantic can express the rule.
+
+## Security
+
+- Keep CORS origins environment-specific.
+- Do not combine wildcard origins with credentialed CORS.
+- Validate JWT expiry, issuer, audience, and algorithm.
+- Rate-limit auth and write-heavy endpoints.
+- Redact credentials, cookies, authorization headers, and tokens from logs.
+
+## Testing
+
+- Override the exact dependency used by `Depends`.
+- Clear `app.dependency_overrides` after tests.
+- Prefer async test clients for async applications.
+
+See skill: `fastapi-patterns`.
diff --git a/.claude/rules/ecc/python/hooks.md b/.claude/rules/ecc/python/hooks.md
new file mode 100644
index 0000000..600c5ea
--- /dev/null
+++ b/.claude/rules/ecc/python/hooks.md
@@ -0,0 +1,19 @@
+---
+paths:
+ - "**/*.py"
+ - "**/*.pyi"
+---
+# Python Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Python specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **black/ruff**: Auto-format `.py` files after edit
+- **mypy/pyright**: Run type checking after editing `.py` files
+
+## Warnings
+
+- Warn about `print()` statements in edited files (use `logging` module instead)
diff --git a/.claude/rules/ecc/python/patterns.md b/.claude/rules/ecc/python/patterns.md
new file mode 100644
index 0000000..5b7f899
--- /dev/null
+++ b/.claude/rules/ecc/python/patterns.md
@@ -0,0 +1,39 @@
+---
+paths:
+ - "**/*.py"
+ - "**/*.pyi"
+---
+# Python Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Python specific content.
+
+## Protocol (Duck Typing)
+
+```python
+from typing import Protocol
+
+class Repository(Protocol):
+ def find_by_id(self, id: str) -> dict | None: ...
+ def save(self, entity: dict) -> dict: ...
+```
+
+## Dataclasses as DTOs
+
+```python
+from dataclasses import dataclass
+
+@dataclass
+class CreateUserRequest:
+ name: str
+ email: str
+ age: int | None = None
+```
+
+## Context Managers & Generators
+
+- Use context managers (`with` statement) for resource management
+- Use generators for lazy evaluation and memory-efficient iteration
+
+## Reference
+
+See skill: `python-patterns` for comprehensive patterns including decorators, concurrency, and package organization.
diff --git a/.claude/rules/ecc/python/security.md b/.claude/rules/ecc/python/security.md
new file mode 100644
index 0000000..e795baf
--- /dev/null
+++ b/.claude/rules/ecc/python/security.md
@@ -0,0 +1,30 @@
+---
+paths:
+ - "**/*.py"
+ - "**/*.pyi"
+---
+# Python Security
+
+> This file extends [common/security.md](../common/security.md) with Python specific content.
+
+## Secret Management
+
+```python
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+api_key = os.environ["OPENAI_API_KEY"] # Raises KeyError if missing
+```
+
+## Security Scanning
+
+- Use **bandit** for static security analysis:
+ ```bash
+ bandit -r src/
+ ```
+
+## Reference
+
+See skill: `django-security` for Django-specific security guidelines (if applicable).
diff --git a/.claude/rules/ecc/python/testing.md b/.claude/rules/ecc/python/testing.md
new file mode 100644
index 0000000..49e3f08
--- /dev/null
+++ b/.claude/rules/ecc/python/testing.md
@@ -0,0 +1,38 @@
+---
+paths:
+ - "**/*.py"
+ - "**/*.pyi"
+---
+# Python Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Python specific content.
+
+## Framework
+
+Use **pytest** as the testing framework.
+
+## Coverage
+
+```bash
+pytest --cov=src --cov-report=term-missing
+```
+
+## Test Organization
+
+Use `pytest.mark` for test categorization:
+
+```python
+import pytest
+
+@pytest.mark.unit
+def test_calculate_total():
+ ...
+
+@pytest.mark.integration
+def test_database_connection():
+ ...
+```
+
+## Reference
+
+See skill: `python-testing` for detailed pytest patterns and fixtures.
diff --git a/.claude/rules/ecc/react/coding-style.md b/.claude/rules/ecc/react/coding-style.md
new file mode 100644
index 0000000..169f314
--- /dev/null
+++ b/.claude/rules/ecc/react/coding-style.md
@@ -0,0 +1,109 @@
+---
+paths:
+ - "**/*.tsx"
+ - "**/*.jsx"
+ - "**/components/**/*.ts"
+ - "**/components/**/*.js"
+ - "**/hooks/**/*.ts"
+ - "**/hooks/**/*.js"
+---
+# React Coding Style
+
+> This file extends [typescript/coding-style.md](../typescript/coding-style.md) and [common/coding-style.md](../common/coding-style.md) with React specific content.
+
+## File Extensions
+
+- `.tsx` for any file containing JSX, even one-liner snippets
+- `.ts` for pure logic, custom hooks without JSX, type definitions, utilities
+- `.test.tsx` / `.test.ts` mirroring the source file
+- Use `.jsx` only when the project intentionally avoids TypeScript — flag every new untyped React file in review
+
+## Naming
+
+- Components: `PascalCase` for both the symbol and the file (`UserCard.tsx`, default export `UserCard`)
+- Custom hooks: `useCamelCase` for the symbol, kebab-case for the file when the project convention is kebab-case (`use-debounce.ts` exports `useDebounce`)
+- Context: `Context` symbol, `Provider` provider component, `use` consumer hook
+- Event handlers: `handleClick`, `handleSubmit` inside the component; the prop that receives it is `onClick`, `onSubmit`
+- Boolean props: `isLoading`, `hasError`, `canSubmit` — never `loading` or `error` alone for booleans
+
+## Component Shape
+
+```tsx
+type Props = {
+ user: User;
+ onSelect: (id: string) => void;
+};
+
+export function UserCard({ user, onSelect }: Props) {
+ return (
+
+ );
+}
+```
+
+- Prefer `type Props = {}` for closed component prop shapes
+- Use `interface` only when the prop type is extended via declaration merging or exported as a public API extension point
+- Always destructure props in the parameter list — no `props.user` access inside the body
+- Type the return implicitly through JSX (`function Foo(): JSX.Element` only when the function returns conditionally and the union confuses inference)
+
+## JSX
+
+- Self-close tags with no children: ``, ``
+- Use fragments `<>...>` over wrapper `
` when no DOM element is needed
+- Conditional rendering: `{condition && }` for booleans, ternary for either/or, early return for guard clauses
+- Never put logic inline in JSX when it reads as multi-line — extract to a const above the return or a function
+
+```tsx
+// Prefer
+const greeting = user.isAdmin ? "Welcome, admin" : `Hello ${user.name}`;
+return
;
+```
+
+## Server / Client Boundary (Next.js App Router, RSC)
+
+- Default a new file to Server Component — only add `"use client"` when the file uses state, effects, refs, browser APIs, or event handlers
+- Place the `"use client"` directive on line 1, before any imports
+- Never import a Client Component file from inside a `"use server"` action file
+- Never re-export server-only code through a client module — the bundler will silently include it
+
+## Imports
+
+- React imports first: `import { useState } from "react"`
+- Then third-party libs, then absolute project imports, then relative
+- Type-only imports: `import type { ReactNode } from "react"` — never mix runtime and type imports in one statement when ESLint's `consistent-type-imports` is configured
+
+## Hooks Discipline
+
+See [hooks.md](./hooks.md) for the full ruleset. Style highlights:
+
+- Custom hooks must start with `use` — enforced by `eslint-plugin-react-hooks`
+- Group all hook calls at the top of the component, before any conditional logic
+- Avoid creating ad-hoc hooks for one-line wrappers — inline the call instead
+
+## State
+
+- Local first (`useState`), lift only when shared
+- Context for cross-cutting state read by many components (theme, auth, i18n) — not for high-frequency updates
+- External store (Zustand, Jotai, Redux Toolkit) when state must persist across route changes, sync across tabs, or be debugged via devtools
+- Never duplicate state that can be derived — compute during render
+
+## Class Components
+
+Forbidden in new code. Convert legacy class components to function components when touching them for non-trivial changes.
+
+## File Layout per Component
+
+```
+components/UserCard/
+ UserCard.tsx
+ UserCard.module.css # or styled-components, or Tailwind classes inline
+ UserCard.test.tsx
+ index.ts # re-export only
+```
+
+Inline single-file components are fine for trivial presentational pieces.
diff --git a/.claude/rules/ecc/react/hooks.md b/.claude/rules/ecc/react/hooks.md
new file mode 100644
index 0000000..a9b9d5a
--- /dev/null
+++ b/.claude/rules/ecc/react/hooks.md
@@ -0,0 +1,187 @@
+---
+paths:
+ - "**/*.tsx"
+ - "**/*.jsx"
+ - "**/hooks/**/*.ts"
+ - "**/hooks/**/*.js"
+ - "**/use-*.ts"
+ - "**/use-*.tsx"
+---
+# React Hooks
+
+> This file covers **React hooks** (`useState`, `useEffect`, `useMemo`, `useCallback`, custom hooks) — NOT the Claude Code `hooks/` runtime system. Naming matches the per-language convention `rules//hooks.md` used across this repo.
+>
+> Extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md).
+
+## Rules of Hooks
+
+Enforce `eslint-plugin-react-hooks` with `react-hooks/rules-of-hooks` set to error.
+
+1. Hooks only at the top level of a function component or another hook
+2. Never in loops, conditionals, nested functions, or after early returns
+3. Always called in the same order on every render
+4. Only inside React function components or custom hooks (functions starting with `use`)
+
+```tsx
+// WRONG: conditional hook
+function Foo({ enabled }: { enabled: boolean }) {
+ if (enabled) {
+ const [x, setX] = useState(0); // rule violation
+ }
+}
+
+// CORRECT: hook unconditional, condition inside
+function Foo({ enabled }: { enabled: boolean }) {
+ const [x, setX] = useState(0);
+ if (!enabled) return null;
+ return {x};
+}
+```
+
+## `useEffect` — When NOT to Use
+
+`useEffect` is for synchronizing with external systems (subscriptions, browser APIs, third-party libraries). It is **not** the right tool for:
+
+- Derived state — compute it during render
+- Transforming data for rendering — compute it during render
+- Resetting state when a prop changes — use a `key` on the parent or derive from props
+- Notifying parents of state changes — call the callback in the event handler
+- Initializing app-level singletons — call the function module-side or in `main.tsx`
+
+```tsx
+// WRONG: effect for derived state
+const [fullName, setFullName] = useState("");
+useEffect(() => {
+ setFullName(`${first} ${last}`);
+}, [first, last]);
+
+// CORRECT: derive during render
+const fullName = `${first} ${last}`;
+```
+
+## Dependency Arrays
+
+- Always include every reactive value referenced inside the effect/callback
+- Enable `react-hooks/exhaustive-deps` lint rule — never silence it without a comment explaining why
+- If the dep array grows unwieldy, the effect is doing too much — split it
+- Stable identity for functions passed in deps: wrap in `useCallback` only when the function is itself a dependency of another hook or passed to a memoized child
+
+## Cleanup
+
+Every subscription, interval, listener, or in-flight request must clean up.
+
+```tsx
+useEffect(() => {
+ const controller = new AbortController();
+ fetch(url, { signal: controller.signal }).then(handleResponse);
+ return () => controller.abort();
+}, [url]);
+```
+
+```tsx
+useEffect(() => {
+ const id = setInterval(tick, 1000);
+ return () => clearInterval(id);
+}, []);
+```
+
+Missing cleanup = race conditions when deps change, memory leaks on unmount.
+
+## `useMemo` and `useCallback` — When Worth It
+
+Default position: **do not memoize**. Add `useMemo` / `useCallback` only when:
+
+1. The value is passed to a `React.memo`-wrapped child as a prop, and identity matters
+2. The value is a dependency of another `useEffect` / `useMemo` / `useCallback`
+3. The computation is measurably expensive (profile before assuming)
+
+Premature memoization adds noise, hides bugs, and can be slower than the recompute it replaces.
+
+## Custom Hooks
+
+Extract a custom hook when:
+
+- The same hook sequence (state + effect + computed) appears in 2+ components
+- The logic has a clear, nameable purpose (`useDebounce`, `useOnClickOutside`, `useLocalStorage`)
+- You want to test the logic independently of any component
+
+Do NOT extract when:
+
+- It would have a single caller — inline it
+- The "hook" is just `useState` with a different name — adds indirection, no value
+
+```tsx
+export function useDebounce(value: T, delay: number): T {
+ const [debounced, setDebounced] = useState(value);
+ useEffect(() => {
+ const id = setTimeout(() => setDebounced(value), delay);
+ return () => clearTimeout(id);
+ }, [value, delay]);
+ return debounced;
+}
+```
+
+## `useState` Patterns
+
+- Initial state from prop only at mount: pass a function `useState(() => computeInitial(prop))` when computation is expensive
+- Functional updater when the new state depends on the old: `setCount(c => c + 1)` — never `setCount(count + 1)` inside async or batched contexts
+- Group related state into one object only when they always change together; otherwise split into multiple `useState` calls
+- Use `useReducer` once state transitions are conditional on the previous state or there are 3+ related values
+
+## `useRef` Patterns
+
+- DOM refs for imperative APIs (focus, scroll, third-party libs)
+- Mutable container that does not trigger re-render (timer ids, previous values, "is mounted" flags)
+- Never read or write `ref.current` during render — only inside effects or event handlers
+- `useImperativeHandle` only when exposing a child API to a parent ref — last-resort escape hatch
+
+## `useSyncExternalStore`
+
+Use this hook to subscribe to any external store (browser API, third-party state lib, custom event emitter). It is the supported way to make external state safe with concurrent rendering.
+
+```tsx
+const isOnline = useSyncExternalStore(
+ (cb) => {
+ window.addEventListener("online", cb);
+ window.addEventListener("offline", cb);
+ return () => {
+ window.removeEventListener("online", cb);
+ window.removeEventListener("offline", cb);
+ };
+ },
+ () => navigator.onLine,
+ () => true,
+);
+```
+
+## React 19 Additions
+
+- `use()` — unwrap promises and contexts inline; usable conditionally (only hook with that property)
+- `useFormStatus()` / `useFormState()` (or `useActionState`) — form submission state without prop drilling
+- `useOptimistic()` — optimistic UI updates while a server action is pending
+- `useTransition()` — mark non-urgent state updates so urgent ones stay responsive
+
+When the project targets React 19+, prefer these over hand-rolled equivalents.
+
+## Stale Closure Trap
+
+Async handlers and intervals capture the values from the render where they were created. Fix by:
+
+1. Using the functional updater form of `setState`
+2. Putting the changing value in the dep array of `useEffect` and rebuilding the handler
+3. Reading from a ref that is kept in sync
+
+## Lint Configuration
+
+Required rules:
+
+```json
+{
+ "rules": {
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn"
+ }
+}
+```
+
+Treat `exhaustive-deps` warnings as errors in CI for new code.
diff --git a/.claude/rules/ecc/react/patterns.md b/.claude/rules/ecc/react/patterns.md
new file mode 100644
index 0000000..8a41293
--- /dev/null
+++ b/.claude/rules/ecc/react/patterns.md
@@ -0,0 +1,194 @@
+---
+paths:
+ - "**/*.tsx"
+ - "**/*.jsx"
+ - "**/components/**/*.ts"
+ - "**/components/**/*.js"
+ - "**/app/**/*.tsx"
+ - "**/pages/**/*.tsx"
+---
+# React Patterns
+
+> This file extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md) with React specific content. For hook-specific rules see [hooks.md](./hooks.md).
+
+## Container / Presentational Split
+
+Container components own data fetching, state, and side effects. Presentational components receive props and render — no service calls, no hooks beyond local UI state.
+
+```tsx
+// Container — owns data
+export function UserPage({ userId }: { userId: string }) {
+ const { data: user, isLoading } = useUser(userId);
+ if (isLoading) return ;
+ if (!user) return ;
+ return ;
+}
+
+// Presentational — pure
+export function UserCard({ user, onSelect }: { user: User; onSelect: (id: string) => void }) {
+ return ;
+}
+```
+
+## State Location Decision Tree
+
+1. Used by one component → `useState` inside it
+2. Used by parent + a few children → lift to nearest common ancestor, pass via props
+3. Used across distant branches → React Context **for low-frequency reads only** (theme, auth, locale)
+4. High-frequency updates shared across the tree → external store (Zustand, Jotai, Redux Toolkit)
+5. Server-derived data → server-state library (TanStack Query, SWR, RSC fetch) — not application state
+
+Context misused for frequently changing values causes every consumer to re-render on every update.
+
+## Server / Client Component Boundary (RSC, Next.js App Router)
+
+- Server Components are the default — they run on the server, do not ship to the client, and can `await` directly
+- Client Components opt in with `"use client"` at the top of the file
+- Data flows down: a Server Component can render a Client Component and pass serializable props
+- A Client Component cannot import a Server Component, but it can receive one via `children` or named slots
+
+```tsx
+// Server (default)
+export default async function Page() {
+ const user = await fetchUser();
+ return ;
+}
+
+// Client
+"use client";
+export function UserClient({ user }: { user: User }) {
+ const [tab, setTab] = useState("profile");
+ return {user.name};
+}
+```
+
+- Never import `"server-only"` packages (DB clients, secrets) from a Client Component file — wrap them in a Server Component or Server Action
+- Mark sensitive modules with `import "server-only"` so the bundler errors if a client file imports them
+
+## Suspense + Error Boundaries
+
+Every Suspense boundary needs an Error Boundary above it. The pair handles both states.
+
+```tsx
+}>
+ }>
+
+
+
+```
+
+- Place Suspense boundaries close to where data is needed, not at the route root
+- Multiple narrower boundaries reveal loaded content progressively
+- Error Boundary must be a Class Component (React 19 has no functional equivalent yet) OR use a library wrapper such as `react-error-boundary`
+
+## Forms
+
+### Uncontrolled (React 19 + form actions)
+
+Prefer uncontrolled inputs with form actions when the form has a clear submit step. The browser owns the value; React reads it via `FormData` on submit.
+
+```tsx
+async function action(formData: FormData) {
+ "use server";
+ await saveUser({ name: String(formData.get("name")) });
+}
+
+export function UserForm() {
+ return (
+
+ );
+}
+```
+
+### Controlled
+
+Use controlled inputs when the value drives other UI, requires real-time validation, or formatting.
+
+```tsx
+const [email, setEmail] = useState("");
+return setEmail(e.target.value)} />;
+```
+
+### Form Libraries
+
+For complex forms (multi-step, dynamic field arrays, cross-field validation), use a library:
+
+- React Hook Form — minimal re-renders, uncontrolled-first
+- TanStack Form — typed, framework-agnostic
+- Final Form — when subscription-based re-renders matter
+
+## Data Fetching
+
+| Strategy | When |
+|---|---|
+| RSC fetch (`await` in Server Component) | Per-request data in Next.js App Router, no client-side cache needed |
+| TanStack Query | Client-side cache, mutations, optimistic updates, polling |
+| SWR | Lightweight cache + revalidation, simpler than TanStack Query |
+| `fetch` in `useEffect` | Avoid — race conditions, no cache, no retry. Only acceptable for one-off fire-and-forget |
+
+Never fetch in a `useEffect` when a real cache library is available — they handle deduping, cache invalidation, error retry, and Suspense integration.
+
+## Lists and Keys
+
+- `key` must be stable across renders — never `index` for any list that can reorder, insert, or delete
+- `key` must be unique among siblings, not globally
+- A reordered list with index keys causes state in child components to attach to the wrong row
+
+## Composition over Inheritance
+
+- Pass `children` for slot-style composition
+- Pass render-prop functions for parameterized rendering
+- Pass component types for plug-in points: `renderItem={UserRow}`
+- Never extend a component class to specialize behavior
+
+## Compound Components
+
+For related controls (Tabs, Accordion, Menu), use compound components sharing state via Context:
+
+```tsx
+
+
+ Profile
+ Settings
+
+
+
+
+```
+
+## Portals
+
+Use `createPortal` for modals, tooltips, toast containers — anything that must escape the parent's `overflow: hidden` or `z-index` stacking context. Render to a stable DOM node mounted in `index.html`.
+
+## Refs and Forwarding (React 19+)
+
+React 19 lets function components accept `ref` as a regular prop — `forwardRef` is no longer required.
+
+```tsx
+export function Input({ ref, ...rest }: { ref?: React.Ref } & InputProps) {
+ return ;
+}
+```
+
+Older codebases on React 18 still need `forwardRef`.
+
+## Out of Scope (Pointer Sections)
+
+### Next.js (App Router)
+
+- Server Actions, Route Handlers, Middleware, Parallel/Intercepted Routes, streaming Metadata
+- Treated as a separate framework concern — when adding deep Next-specific patterns, propose a dedicated `rules/nextjs/` track
+- For now follow Next.js official docs for App Router specifics
+
+### React Native
+
+- Platform-specific imports (`Platform.OS`, `.ios.tsx` / `.android.tsx`), `StyleSheet`, navigation libraries (React Navigation, Expo Router)
+- Treated as a separate track — `rules/react-native/` is not yet present
+- React core hooks/patterns from this file still apply
+
+## Skill Reference
+
+For React-specific deep dives see `skills/react-patterns/SKILL.md`. For cross-framework frontend concerns see `skills/frontend-patterns/SKILL.md`. For accessibility see `skills/accessibility/SKILL.md`.
diff --git a/.claude/rules/ecc/react/security.md b/.claude/rules/ecc/react/security.md
new file mode 100644
index 0000000..1e3553e
--- /dev/null
+++ b/.claude/rules/ecc/react/security.md
@@ -0,0 +1,180 @@
+---
+paths:
+ - "**/*.tsx"
+ - "**/*.jsx"
+ - "**/components/**/*.ts"
+ - "**/app/**/*.ts"
+ - "**/pages/**/*.ts"
+---
+# React Security
+
+> This file extends [typescript/security.md](../typescript/security.md) and [common/security.md](../common/security.md) with React specific content.
+
+## XSS via `dangerouslySetInnerHTML`
+
+CRITICAL. The prop name is deliberately scary — treat every usage as a code review halt.
+
+```tsx
+// CRITICAL: unsanitized user input
+
+
+// CORRECT options:
+// 1. Render as text
+
{userBio}
+
+// 2. Render parsed markdown via a library that sanitizes
+{userBio}
+
+// 3. If raw HTML is required, sanitize first with DOMPurify
+import DOMPurify from "isomorphic-dompurify";
+
+```
+
+Audit checklist for every `dangerouslySetInnerHTML` call:
+
+- Is the input always under our control? Document the source.
+- If user-derived: is it sanitized at the **same call site**? (Sanitization at the API boundary is acceptable only if every consumer is verified.)
+- Is the sanitizer config allowlisting tags, not denylisting?
+
+## Unsafe URL Schemes
+
+`javascript:` and `data:` URLs in `href`, `src`, and `xlink:href` execute arbitrary code.
+
+```tsx
+// CRITICAL: javascript: URL injection
+Visit // if user.website = "javascript:alert(1)"
+
+// CORRECT: validate scheme
+function safeUrl(url: string): string | undefined {
+ try {
+ const parsed = new URL(url);
+ if (["http:", "https:", "mailto:"].includes(parsed.protocol)) return url;
+ } catch {
+ return undefined;
+ }
+ return undefined;
+}
+Visit
+```
+
+React warns about `javascript:` URLs in `href` in development mode, but does not block them at runtime. `data:` URLs and other schemes also slip through. Always validate.
+
+## `target="_blank"` Without `rel`
+
+`` without `rel="noopener noreferrer"` lets the target page access `window.opener` and run navigation hijacks.
+
+```tsx
+// WRONG
+External
+
+// CORRECT
+External
+```
+
+Modern browsers default to `noopener` when `target="_blank"`, but do not rely on browser defaults — be explicit.
+
+## Server Action Input Validation
+
+Server Actions (`"use server"`) run with the same trust level as a public API endpoint. Validate every input.
+
+```tsx
+"use server";
+import { z } from "zod";
+
+const Input = z.object({
+ email: z.string().email(),
+ age: z.number().int().min(0).max(120),
+});
+
+export async function updateUser(_state: unknown, formData: FormData) {
+ const parsed = Input.safeParse({
+ email: formData.get("email"),
+ age: Number(formData.get("age")),
+ });
+ if (!parsed.success) return { error: parsed.error.flatten() };
+ // ...
+}
+```
+
+- Authenticate inside the action — do not trust the client-side route gate
+- Authorize: confirm the current user has permission for the specific record they are mutating
+- Rate limit sensitive actions
+
+## Secret Exposure via Env Vars
+
+Prefixed env vars are bundled into the client. Treat them as public.
+
+| Framework | Public prefix | Private |
+|---|---|---|
+| Next.js | `NEXT_PUBLIC_*` | All others |
+| Vite | `VITE_*` | `.env` server-side only |
+| Create React App | `REACT_APP_*`, plus `NODE_ENV` and `PUBLIC_URL` | All others (anything without the `REACT_APP_` prefix is server-side only) |
+| Remix | `process.env` access in `loader`/`action` only | Same |
+
+```ts
+// CRITICAL: secret leaked to client bundle
+const apiKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;
+```
+
+Audit on every PR that touches env vars: would this string in the public bundle be a problem?
+
+## Authentication / Authorization
+
+- Never store sessions in `localStorage` — accessible to any XSS. Use httpOnly secure cookies.
+- Never trust client-set state to gate sensitive UI. Render-gating in JSX prevents display, not access — the API must enforce.
+- CSRF: cookie-based auth requires CSRF tokens or `SameSite=Strict`/`Lax` cookies
+- Use double-submit cookies or origin verification for form actions when not using framework defaults
+
+## Content Security Policy (CSP)
+
+Configure server-side. The minimum acceptable CSP for a React app:
+
+```
+default-src 'self';
+script-src 'self' 'nonce-{REQUEST_NONCE}';
+style-src 'self' 'unsafe-inline';
+img-src 'self' data: https:;
+connect-src 'self' https://api.example.com;
+frame-ancestors 'none';
+```
+
+- Avoid `unsafe-inline` and `unsafe-eval` in `script-src`
+- For SSR with inline scripts (Next.js streaming, hydration data), use per-request nonces — both Next.js and Remix support nonce injection
+- `style-src 'unsafe-inline'` is often unavoidable for CSS-in-JS libraries — document the tradeoff
+
+## Prototype Pollution via Object Spread
+
+```tsx
+// WRONG: untrusted JSON spread directly into state
+const update = await req.json();
+setState({ ...state, ...update }); // attacker controls __proto__
+
+// CORRECT: parse with a schema, or guard keys
+const Allowed = z.object({ name: z.string(), email: z.string().email() });
+const parsed = Allowed.parse(await req.json());
+setState({ ...state, ...parsed });
+```
+
+## SSR Template Injection
+
+When using `renderToString` or `renderToPipeableStream`:
+
+- All values rendered inside JSX are escaped by React — safe
+- Values passed to `dangerouslySetInnerHTML` are NOT escaped — same rules as client
+- Manually constructed HTML wrappers around the React output must be escaped or sanitized — never concatenate user input into the surrounding HTML template
+
+## Third-Party Components
+
+- Audit `npm audit` before adding any UI library
+- Check that the library does not internally use `dangerouslySetInnerHTML` on its input (e.g., rich text editors)
+- Pin versions, review changelogs before major upgrades
+- Be wary of components that accept HTML strings as props
+
+## Source Map Exposure in Production
+
+Production builds should ship without source maps, or with sourcemaps uploaded to an error tracker (Sentry) and stripped from the public bundle. Public source maps leak internal logic and file structure.
+
+## Agent Support
+
+- Use `security-reviewer` agent for comprehensive security audits across the codebase
+- Use `react-reviewer` agent for React-specific patterns and the above rules in active code review
diff --git a/.claude/rules/ecc/react/testing.md b/.claude/rules/ecc/react/testing.md
new file mode 100644
index 0000000..fa8da66
--- /dev/null
+++ b/.claude/rules/ecc/react/testing.md
@@ -0,0 +1,208 @@
+---
+paths:
+ - "**/*.test.tsx"
+ - "**/*.test.jsx"
+ - "**/*.spec.tsx"
+ - "**/*.spec.jsx"
+ - "**/__tests__/**/*.ts"
+ - "**/__tests__/**/*.tsx"
+---
+# React Testing
+
+> This file extends [typescript/testing.md](../typescript/testing.md) and [common/testing.md](../common/testing.md) with React specific content.
+
+## Library Choice
+
+- **React Testing Library (RTL)** — the standard for component testing. Tests behavior through the rendered DOM.
+- **Vitest** — preferred runner for new Vite-based projects. Faster than Jest, native ESM, same API.
+- **Jest** — still the default for Next.js / CRA projects. RTL works identically.
+- **Playwright Component Testing** — when component tests need a real browser engine (animation, layout, complex events)
+- **Cypress Component Testing** — alternative real-browser component runner
+
+Pick one component test runner per project — do not mix RTL + Playwright CT in the same repo.
+
+## Core Principle
+
+Test what the user sees and does, not implementation details.
+
+- Query by accessible role first, then label, then text — fall back to `data-testid` only when nothing else fits
+- Never assert on internal state, props passed to children, or which hooks were called
+- Refactor without breaking tests = the test was testing behavior; that is the goal
+
+## Query Priority
+
+RTL exposes queries in three families. Use this priority order top-down:
+
+1. **Accessible to everyone**
+ - `getByRole(role, { name })` — primary choice
+ - `getByLabelText` — for form inputs
+ - `getByPlaceholderText` — when no label is available (and add a label)
+ - `getByText` — for non-interactive text
+ - `getByDisplayValue` — for form fields with a current value
+
+2. **Semantic queries**
+ - `getByAltText` — for images
+ - `getByTitle` — last resort, low accessibility value
+
+3. **Test IDs**
+ - `getByTestId("some-id")` — escape hatch only, when none of the above work
+
+`getBy*` throws when no match. `queryBy*` returns null (use for asserting absence). `findBy*` returns a promise (use for async).
+
+## User Interaction
+
+Prefer `userEvent` over `fireEvent`. `userEvent` simulates real browser sequences (focus, keydown, beforeinput, input, keyup) — `fireEvent` dispatches a single synthetic event.
+
+```tsx
+import userEvent from "@testing-library/user-event";
+
+test("submits the form", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.type(screen.getByLabelText("Email"), "user@example.com");
+ await user.click(screen.getByRole("button", { name: /save/i }));
+
+ expect(handleSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
+});
+```
+
+- Always `await` `userEvent` calls — they are async
+- Call `userEvent.setup()` once at the top of each test, then reuse the returned `user`
+
+## Async Assertions
+
+```tsx
+// WRONG: synchronous query for async-rendered content
+expect(screen.getByText("Loaded")).toBeInTheDocument(); // throws — not in DOM yet
+
+// CORRECT: findBy* (returns a promise, retries)
+expect(await screen.findByText("Loaded")).toBeInTheDocument();
+
+// CORRECT: waitFor for non-element assertions
+await waitFor(() => expect(saveSpy).toHaveBeenCalled());
+```
+
+- `findBy*` for async element appearance
+- `waitFor` for async expectations on side effects or other matchers
+- Never `setTimeout` + assertion — flaky
+
+## Network Mocking with MSW
+
+Use Mock Service Worker for any test that hits a network boundary. MSW runs at the network layer, so the component, hooks, and fetch library all behave as in production.
+
+```tsx
+// test setup
+import { setupServer } from "msw/node";
+import { http, HttpResponse } from "msw";
+
+const server = setupServer(
+ http.get("/api/users/:id", ({ params }) =>
+ HttpResponse.json({ id: params.id, name: "Alice" }),
+ ),
+);
+
+beforeAll(() => server.listen());
+afterEach(() => server.resetHandlers());
+afterAll(() => server.close());
+```
+
+Per-test override:
+
+```tsx
+test("renders error on 500", async () => {
+ server.use(http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })));
+ render();
+ expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
+});
+```
+
+## Avoid Snapshot Tests for Components
+
+Snapshots of rendered output are brittle, hard to review, and rubber-stamped by reviewers. Use them only for:
+
+- Pure data serialization (e.g., a transformer that produces a stable string)
+- Catching unintended regressions in non-visual output
+
+For component visual regression, use Playwright / Cypress / Percy screenshots — actual visual diffs, not DOM diffs.
+
+## Test Setup Helpers
+
+Wrap providers once:
+
+```tsx
+function renderWithProviders(ui: React.ReactElement) {
+ return render(
+
+
+ {ui}
+
+ ,
+ );
+}
+```
+
+Export from `test-utils.tsx` and use everywhere.
+
+## Custom Hook Testing
+
+Use `renderHook` from RTL:
+
+```tsx
+import { renderHook, act } from "@testing-library/react";
+
+test("useCounter increments", () => {
+ const { result } = renderHook(() => useCounter());
+ act(() => result.current.increment());
+ expect(result.current.count).toBe(1);
+});
+```
+
+- Always wrap state-changing calls in `act`
+- Always test through the public hook API, not internal implementation
+
+## Accessibility Assertions
+
+```tsx
+import { axe } from "vitest-axe"; // or jest-axe
+
+test("UserCard has no a11y violations", async () => {
+ const { container } = render();
+ expect(await axe(container)).toHaveNoViolations();
+});
+```
+
+Run axe assertions in component tests — catches missing labels, ARIA misuse, color contrast (limited).
+
+## When to Reach for Playwright / Cypress
+
+Component test with RTL + JSDOM cannot:
+
+- Test real layout (flexbox, grid, viewport-dependent rendering)
+- Test scrolling, drag-and-drop, paste from clipboard
+- Test browser-native animation, CSS transitions
+- Test cross-frame interactions (iframes, popups)
+
+For those, use Playwright Component Testing or end-to-end Playwright/Cypress runs. See [e2e-testing skill](../../skills/e2e-testing/SKILL.md).
+
+## Coverage Targets
+
+| Layer | Target |
+|---|---|
+| Pure utility functions | ≥90% |
+| Custom hooks | ≥85% |
+| Components (presentational) | ≥80% — behavior, not lines |
+| Container components | ≥70% — golden paths + error states |
+| Pages (E2E covered separately) | Smoke test per route minimum |
+
+## Anti-Patterns
+
+- Asserting on `container.querySelector` — bypasses accessibility queries
+- Asserting on number of renders — implementation detail
+- Mocking React hooks (`jest.mock("react", ...)`) — refactor the component instead
+- Mocking child components by default — tests the integration, not the parent in isolation
+- Manual `act()` warnings ignored — they indicate real bugs
+
+## Skill Reference
+
+See `skills/react-testing/SKILL.md` for end-to-end test examples, MSW patterns, and accessibility test scaffolding.
diff --git a/.claude/rules/ecc/ruby/coding-style.md b/.claude/rules/ecc/ruby/coding-style.md
new file mode 100644
index 0000000..39506bb
--- /dev/null
+++ b/.claude/rules/ecc/ruby/coding-style.md
@@ -0,0 +1,46 @@
+---
+paths:
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/Gemfile"
+ - "**/*.gemspec"
+ - "**/config.ru"
+---
+# Ruby Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Ruby and Rails specific content.
+
+## Standards
+
+- Target **Ruby 3.3+** for new Rails work unless the project already pins an older supported runtime.
+- Enable **YJIT** in production only after measuring boot time, memory, and request/job throughput.
+- Add `# frozen_string_literal: true` to new Ruby files when the project uses that convention.
+- Prefer clear Ruby over clever metaprogramming; isolate DSL-heavy code behind narrow, tested boundaries.
+
+## Formatting And Linting
+
+- Use the project's checked-in RuboCop config. For Rails 8+ apps, start from `rubocop-rails-omakase` and customize only where the codebase has a real convention.
+- Keep formatter/linter commands behind binstubs or scripts so CI and local runs match:
+
+```bash
+bundle exec rubocop
+bundle exec rubocop -A
+```
+
+- Do not silence cops inline unless the exception is narrow, documented, and harder to express cleanly in code.
+
+## Rails Style
+
+- Follow Rails naming and directory conventions before adding custom structure.
+- Keep controllers transport-focused: authentication, authorization, parameter handling, response shape.
+- Put reusable domain behavior in models, concerns, service objects, query objects, or form objects based on actual complexity, not as default ceremony.
+- Prefer `bin/rails`, `bin/rake`, and checked-in binstubs over globally installed commands.
+
+## Error Handling
+
+- Rescue specific exceptions. Avoid broad `rescue StandardError` blocks unless they re-raise or preserve enough context for operators.
+- Use `ActiveSupport::Notifications` or the app's logger for operational events; do not leave `puts`, `pp`, or `debugger` in committed application code.
+
+## Reference
+
+See skill: `backend-patterns` for broader service/repository layering guidance.
diff --git a/.claude/rules/ecc/ruby/hooks.md b/.claude/rules/ecc/ruby/hooks.md
new file mode 100644
index 0000000..1ec61d8
--- /dev/null
+++ b/.claude/rules/ecc/ruby/hooks.md
@@ -0,0 +1,37 @@
+---
+paths:
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/Gemfile"
+ - "**/Gemfile.lock"
+ - "**/config/routes.rb"
+---
+# Ruby Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Ruby and Rails specific content.
+
+## PostToolUse Hooks
+
+Configure project-local hooks to prefer binstubs and checked-in tooling:
+
+- **RuboCop**: run `bundle exec rubocop -A ` or the project's safer formatter command after Ruby edits.
+- **Brakeman**: run `bundle exec brakeman --no-progress` after security-sensitive Rails changes.
+- **Tests**: run the narrowest matching `bin/rails test ...` or `bundle exec rspec ...` command for touched files.
+- **Bundler audit**: run `bundle exec bundle-audit check --update` when `Gemfile` or `Gemfile.lock` changes and the project has bundler-audit installed.
+
+## Warnings
+
+- Warn on committed `debugger`, `binding.irb`, `binding.pry`, `puts`, `pp`, or `p` calls in application code.
+- Warn when an edit disables CSRF protection, expands mass-assignment, or adds raw SQL without parameterization.
+- Warn when a migration changes data destructively without a reversible path or documented rollout plan.
+
+## CI Gate Suggestions
+
+```bash
+bundle exec rubocop
+bundle exec brakeman --no-progress
+bin/rails test
+bundle exec rspec
+```
+
+Use only the commands that are present in the project; do not install new hook dependencies without maintainer approval.
diff --git a/.claude/rules/ecc/ruby/patterns.md b/.claude/rules/ecc/ruby/patterns.md
new file mode 100644
index 0000000..e405384
--- /dev/null
+++ b/.claude/rules/ecc/ruby/patterns.md
@@ -0,0 +1,44 @@
+---
+paths:
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/Gemfile"
+ - "**/app/**/*.erb"
+ - "**/config/routes.rb"
+---
+# Ruby Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Ruby and Rails specific content.
+
+## Rails Way First
+
+- Start with plain Rails MVC and Active Record conventions for small and medium features.
+- Introduce service objects, query objects, form objects, decorators, or presenters when the model/controller boundary is carrying multiple responsibilities.
+- Name extracted objects after the business operation they perform, not after generic layers like `Manager` or `Processor`.
+
+## Persistence
+
+- Prefer PostgreSQL for multi-host production Rails apps unless the existing platform has a clear reason for MySQL or SQLite.
+- Treat Rails 8 SQLite-backed defaults as viable for single-host or modest deployments, not as an automatic fit for shared multi-service systems.
+- Keep raw SQL behind query objects or model scopes and parameterize every dynamic value.
+
+## Background Jobs And Runtime Services
+
+- Use **Solid Queue** for greenfield Rails 8 apps with modest throughput and simple deployment needs.
+- Use **Sidekiq** when the app needs mature observability, high throughput, existing Redis infrastructure, or Pro/Enterprise features.
+- Use **Solid Cache** and **Solid Cable** when their deployment model matches the app; use Redis when shared cross-service behavior, high fanout, or advanced data structures matter.
+
+## Frontend
+
+- Prefer **Hotwire** with Turbo, Stimulus, Importmap, and Propshaft for server-rendered Rails apps.
+- Use React, Vue, Inertia.js, or a separate SPA when interaction complexity, existing product architecture, or team ownership justifies the extra client surface.
+- Keep view components, partials, and presenters focused on rendering decisions; keep persistence and authorization out of templates.
+
+## Authentication
+
+- Use the Rails 8 authentication generator for straightforward session auth and password reset needs.
+- Use Devise or another established auth system when requirements include OAuth, MFA, confirmable/lockable flows, multi-model auth, or a large existing Devise footprint.
+
+## Reference
+
+See skill: `backend-patterns` for service boundaries and adapter patterns.
diff --git a/.claude/rules/ecc/ruby/security.md b/.claude/rules/ecc/ruby/security.md
new file mode 100644
index 0000000..4821c2c
--- /dev/null
+++ b/.claude/rules/ecc/ruby/security.md
@@ -0,0 +1,51 @@
+---
+paths:
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/Gemfile"
+ - "**/Gemfile.lock"
+ - "**/config/routes.rb"
+ - "**/config/credentials*.yml.enc"
+---
+# Ruby Security
+
+> This file extends [common/security.md](../common/security.md) with Ruby and Rails specific content.
+
+## Rails Defaults
+
+- Keep CSRF protection enabled for state-changing browser requests.
+- Use strong parameters or typed boundary objects before mass assignment.
+- Store secrets in Rails credentials, environment variables, or a secret manager. Never commit plaintext keys, tokens, private credentials, or copied `.env` values.
+
+## SQL And Active Record
+
+- Prefer Active Record query APIs and parameterized SQL.
+- Never interpolate request, cookie, header, job, or webhook values into SQL strings.
+- Scope model callbacks carefully; security-sensitive side effects should be explicit and covered by tests.
+
+## Authentication And Sessions
+
+- Use the Rails 8 authentication generator for simple session auth, or Devise when OAuth, MFA, confirmable, lockable, multi-model auth, or existing Devise conventions are required.
+- Rotate sessions after sign-in and privilege changes.
+- Protect account recovery flows with expiry, single-use tokens, rate limiting, and audit logging.
+
+## Dependencies
+
+- Run dependency checks when the lockfile changes:
+
+```bash
+bundle exec bundle-audit check --update
+bundle exec brakeman --no-progress
+```
+
+- Review new gems for maintainer activity, native extension risk, transitive dependencies, and whether the same behavior can be implemented with Rails core.
+
+## Web Safety
+
+- Escape template output by default. Treat `html_safe`, `raw`, and custom sanitizers as security-sensitive code.
+- Validate file uploads by content type, extension, size, and storage destination.
+- Treat background jobs, webhooks, Action Cable messages, and Turbo Stream inputs as untrusted boundaries.
+
+## Reference
+
+See skill: `security-review` for secure-by-default review patterns.
diff --git a/.claude/rules/ecc/ruby/testing.md b/.claude/rules/ecc/ruby/testing.md
new file mode 100644
index 0000000..e96a1c9
--- /dev/null
+++ b/.claude/rules/ecc/ruby/testing.md
@@ -0,0 +1,51 @@
+---
+paths:
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/Gemfile"
+ - "**/test/**/*.rb"
+ - "**/spec/**/*.rb"
+ - "**/config/routes.rb"
+---
+# Ruby Testing
+
+> This file extends [common/testing.md](../common/testing.md) with Ruby and Rails specific content.
+
+## Framework
+
+- Use **Minitest** when the Rails app follows the default Rails test stack.
+- Use **RSpec** when it is already established in the project or the team has explicit production conventions around it.
+- Do not mix Minitest and RSpec inside the same feature area without a migration reason.
+
+## Test Pyramid
+
+- Put fast domain behavior in model, service, query, policy, and job tests.
+- Use request/controller tests for HTTP contracts, auth behavior, redirects, status codes, and response shapes.
+- Use system tests with Capybara for browser-critical flows only; keep them focused and stable.
+- Cover background jobs with unit tests for behavior and integration tests for queue/enqueue contracts.
+
+## Fixtures And Factories
+
+- Use Rails fixtures when they are the project default and the data graph is small.
+- Use `factory_bot` when scenarios need explicit object construction or complex traits.
+- Keep test data close to the behavior being asserted; avoid global fixtures that hide setup cost.
+
+## Commands
+
+Prefer project-local commands:
+
+```bash
+bin/rails test
+bin/rails test test/models/user_test.rb
+bundle exec rspec
+bundle exec rspec spec/models/user_spec.rb
+```
+
+## Coverage
+
+- Use SimpleCov when coverage is enforced; keep thresholds in CI and avoid gaming branch coverage with low-value tests.
+- Add regression tests for bug fixes before changing production code.
+
+## Reference
+
+See skill: `tdd-workflow` for the repo-wide RED -> GREEN -> REFACTOR loop.
diff --git a/.claude/rules/ecc/rust/coding-style.md b/.claude/rules/ecc/rust/coding-style.md
new file mode 100644
index 0000000..cda67b5
--- /dev/null
+++ b/.claude/rules/ecc/rust/coding-style.md
@@ -0,0 +1,151 @@
+---
+paths:
+ - "**/*.rs"
+---
+# Rust Coding Style
+
+> This file extends [common/coding-style.md](../common/coding-style.md) with Rust-specific content.
+
+## Formatting
+
+- **rustfmt** for enforcement — always run `cargo fmt` before committing
+- **clippy** for lints — `cargo clippy -- -D warnings` (treat warnings as errors)
+- 4-space indent (rustfmt default)
+- Max line width: 100 characters (rustfmt default)
+
+## Immutability
+
+Rust variables are immutable by default — embrace this:
+
+- Use `let` by default; only use `let mut` when mutation is required
+- Prefer returning new values over mutating in place
+- Use `Cow<'_, T>` when a function may or may not need to allocate
+
+```rust
+use std::borrow::Cow;
+
+// GOOD — immutable by default, new value returned
+fn normalize(input: &str) -> Cow<'_, str> {
+ if input.contains(' ') {
+ Cow::Owned(input.replace(' ', "_"))
+ } else {
+ Cow::Borrowed(input)
+ }
+}
+
+// BAD — unnecessary mutation
+fn normalize_bad(input: &mut String) {
+ *input = input.replace(' ', "_");
+}
+```
+
+## Naming
+
+Follow standard Rust conventions:
+- `snake_case` for functions, methods, variables, modules, crates
+- `PascalCase` (UpperCamelCase) for types, traits, enums, type parameters
+- `SCREAMING_SNAKE_CASE` for constants and statics
+- Lifetimes: short lowercase (`'a`, `'de`) — descriptive names for complex cases (`'input`)
+
+## Ownership and Borrowing
+
+- Borrow (`&T`) by default; take ownership only when you need to store or consume
+- Never clone to satisfy the borrow checker without understanding the root cause
+- Accept `&str` over `String`, `&[T]` over `Vec` in function parameters
+- Use `impl Into` for constructors that need to own a `String`
+
+```rust
+// GOOD — borrows when ownership isn't needed
+fn word_count(text: &str) -> usize {
+ text.split_whitespace().count()
+}
+
+// GOOD — takes ownership in constructor via Into
+fn new(name: impl Into) -> Self {
+ Self { name: name.into() }
+}
+
+// BAD — takes String when &str suffices
+fn word_count_bad(text: String) -> usize {
+ text.split_whitespace().count()
+}
+```
+
+## Error Handling
+
+- Use `Result` and `?` for propagation — never `unwrap()` in production code
+- **Libraries**: define typed errors with `thiserror`
+- **Applications**: use `anyhow` for flexible error context
+- Add context with `.with_context(|| format!("failed to ..."))?`
+- Reserve `unwrap()` / `expect()` for tests and truly unreachable states
+
+```rust
+// GOOD — library error with thiserror
+#[derive(Debug, thiserror::Error)]
+pub enum ConfigError {
+ #[error("failed to read config: {0}")]
+ Io(#[from] std::io::Error),
+ #[error("invalid config format: {0}")]
+ Parse(String),
+}
+
+// GOOD — application error with anyhow
+use anyhow::Context;
+
+fn load_config(path: &str) -> anyhow::Result {
+ let content = std::fs::read_to_string(path)
+ .with_context(|| format!("failed to read {path}"))?;
+ toml::from_str(&content)
+ .with_context(|| format!("failed to parse {path}"))
+}
+```
+
+## Iterators Over Loops
+
+Prefer iterator chains for transformations; use loops for complex control flow:
+
+```rust
+// GOOD — declarative and composable
+let active_emails: Vec<&str> = users.iter()
+ .filter(|u| u.is_active)
+ .map(|u| u.email.as_str())
+ .collect();
+
+// GOOD — loop for complex logic with early returns
+for user in &users {
+ if let Some(verified) = verify_email(&user.email)? {
+ send_welcome(&verified)?;
+ }
+}
+```
+
+## Module Organization
+
+Organize by domain, not by type:
+
+```text
+src/
+├── main.rs
+├── lib.rs
+├── auth/ # Domain module
+│ ├── mod.rs
+│ ├── token.rs
+│ └── middleware.rs
+├── orders/ # Domain module
+│ ├── mod.rs
+│ ├── model.rs
+│ └── service.rs
+└── db/ # Infrastructure
+ ├── mod.rs
+ └── pool.rs
+```
+
+## Visibility
+
+- Default to private; use `pub(crate)` for internal sharing
+- Only mark `pub` what is part of the crate's public API
+- Re-export public API from `lib.rs`
+
+## References
+
+See skill: `rust-patterns` for comprehensive Rust idioms and patterns.
diff --git a/.claude/rules/ecc/rust/hooks.md b/.claude/rules/ecc/rust/hooks.md
new file mode 100644
index 0000000..4511f1c
--- /dev/null
+++ b/.claude/rules/ecc/rust/hooks.md
@@ -0,0 +1,16 @@
+---
+paths:
+ - "**/*.rs"
+ - "**/Cargo.toml"
+---
+# Rust Hooks
+
+> This file extends [common/hooks.md](../common/hooks.md) with Rust-specific content.
+
+## PostToolUse Hooks
+
+Configure in `~/.claude/settings.json`:
+
+- **cargo fmt**: Auto-format `.rs` files after edit
+- **cargo clippy**: Run lint checks after editing Rust files
+- **cargo check**: Verify compilation after changes (faster than `cargo build`)
diff --git a/.claude/rules/ecc/rust/patterns.md b/.claude/rules/ecc/rust/patterns.md
new file mode 100644
index 0000000..3d807e7
--- /dev/null
+++ b/.claude/rules/ecc/rust/patterns.md
@@ -0,0 +1,168 @@
+---
+paths:
+ - "**/*.rs"
+---
+# Rust Patterns
+
+> This file extends [common/patterns.md](../common/patterns.md) with Rust-specific content.
+
+## Repository Pattern with Traits
+
+Encapsulate data access behind a trait:
+
+```rust
+pub trait OrderRepository: Send + Sync {
+ fn find_by_id(&self, id: u64) -> Result
{isLoading ? (
diff --git a/src/app/p/[secret]/posts/page.tsx b/src/app/p/[secret]/posts/page.tsx
index b8d4665..58b8dd9 100644
--- a/src/app/p/[secret]/posts/page.tsx
+++ b/src/app/p/[secret]/posts/page.tsx
@@ -104,10 +104,10 @@ export default function AdminPostsPage() {
{post.caption ?? No caption}