thevoidforge 21.0.11 → 21.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/wizard/ui/index.html +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Pattern: Middleware (Auth + Request Logging)
3
+ *
4
+ * Key principles:
5
+ * - Auth middleware runs before route handlers
6
+ * - Request logging captures method, path, status, duration
7
+ * - Structured JSON logs with requestId for tracing
8
+ * - Never log sensitive data (passwords, tokens, PII)
9
+ * - Fail closed — if auth check errors, deny access
10
+ *
11
+ * Agents: Kenobi (security), Barton (error handling), L (monitoring)
12
+ *
13
+ * Framework adaptations:
14
+ * Next.js: This file (middleware.ts in root, NextRequest/NextResponse)
15
+ * Express: app.use(authMiddleware), app.use(requestLogger) — (err, req, res, next) for errors
16
+ * Django: MIDDLEWARE list in settings.py, or decorators (@login_required, @permission_required)
17
+ * Rails: before_action in controllers, Rack middleware for logging
18
+ *
19
+ * === Django Deep Dive ===
20
+ *
21
+ * # app/middleware.py
22
+ * import time, json, logging, uuid
23
+ *
24
+ * class RequestLoggingMiddleware:
25
+ * def __init__(self, get_response):
26
+ * self.get_response = get_response
27
+ * def __call__(self, request):
28
+ * request.request_id = str(uuid.uuid4())
29
+ * start = time.monotonic()
30
+ * response = self.get_response(request)
31
+ * duration = time.monotonic() - start
32
+ * logging.info(json.dumps({
33
+ * "request_id": request.request_id, "method": request.method,
34
+ * "path": request.path, "status": response.status_code,
35
+ * "duration_ms": round(duration * 1000),
36
+ * "user_id": getattr(request.user, "id", None),
37
+ * }))
38
+ * return response
39
+ *
40
+ * # settings.py: MIDDLEWARE = ['app.middleware.RequestLoggingMiddleware', ...]
41
+ * # Order matters: logging before auth, auth before rate limiting
42
+ * # @login_required is per-view — use for fine-grained control, not global middleware
43
+ *
44
+ * === FastAPI Deep Dive ===
45
+ *
46
+ * from fastapi import Request
47
+ * from starlette.middleware.base import BaseHTTPMiddleware
48
+ * import time, uuid
49
+ *
50
+ * class RequestLoggingMiddleware(BaseHTTPMiddleware):
51
+ * async def dispatch(self, request: Request, call_next):
52
+ * request.state.request_id = str(uuid.uuid4())
53
+ * start = time.monotonic()
54
+ * response = await call_next(request)
55
+ * duration = time.monotonic() - start
56
+ * # structured log here
57
+ * return response
58
+ *
59
+ * # app.add_middleware(RequestLoggingMiddleware)
60
+ * # For auth: prefer Depends() over middleware — dependency injection is more testable
61
+ * # For rate limiting: slowapi or custom Depends() with Redis
62
+ */
63
+
64
+ import { NextRequest, NextResponse } from 'next/server'
65
+ import { verifySessionToken } from '@/lib/auth'
66
+ import { nanoid } from 'nanoid'
67
+
68
+ // --- Auth middleware ---
69
+ export async function authMiddleware(req: NextRequest) {
70
+ const token = req.cookies.get('session')?.value
71
+
72
+ if (!token) {
73
+ return NextResponse.json(
74
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
75
+ { status: 401 }
76
+ )
77
+ }
78
+
79
+ try {
80
+ // Fail closed — if verification throws, deny access (Kenobi)
81
+ const session = await verifySessionToken(token)
82
+
83
+ if (!session) {
84
+ return NextResponse.json(
85
+ { error: { code: 'UNAUTHORIZED', message: 'Invalid session' } },
86
+ { status: 401 }
87
+ )
88
+ }
89
+
90
+ // Attach session to request headers for downstream use
91
+ const response = NextResponse.next()
92
+ response.headers.set('x-user-id', session.userId)
93
+ response.headers.set('x-request-id', nanoid())
94
+ return response
95
+ } catch {
96
+ // Auth errors = deny, don't expose details
97
+ return NextResponse.json(
98
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
99
+ { status: 401 }
100
+ )
101
+ }
102
+ }
103
+
104
+ // --- Request logging middleware ---
105
+ export function withRequestLogging(
106
+ handler: (req: NextRequest) => Promise<NextResponse>
107
+ ) {
108
+ return async (req: NextRequest): Promise<NextResponse> => {
109
+ const requestId = nanoid()
110
+ const start = Date.now()
111
+
112
+ // Add request ID for tracing
113
+ const url = new URL(req.url)
114
+
115
+ let response: NextResponse
116
+ let error: unknown = null
117
+
118
+ try {
119
+ response = await handler(req)
120
+ } catch (err) {
121
+ error = err
122
+ response = NextResponse.json(
123
+ { error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' } },
124
+ { status: 500 }
125
+ )
126
+ }
127
+
128
+ const duration = Date.now() - start
129
+
130
+ // Structured log — JSON, parseable, includes tracing context (L — monitoring)
131
+ const logEntry = {
132
+ requestId,
133
+ method: req.method,
134
+ path: url.pathname,
135
+ status: response.status,
136
+ duration,
137
+ userId: req.headers.get('x-user-id') || undefined,
138
+ // Never log: request body, auth tokens, cookies, PII (Padme — data protection)
139
+ ...(error instanceof Error && { error: error.message }),
140
+ ...(duration > 1000 && { slow: true }), // Flag slow requests
141
+ }
142
+
143
+ if (response.status >= 500) {
144
+ console.error(JSON.stringify(logEntry))
145
+ } else if (response.status >= 400) {
146
+ console.warn(JSON.stringify(logEntry))
147
+ } else {
148
+ console.log(JSON.stringify(logEntry))
149
+ }
150
+
151
+ // Attach request ID to response for client-side debugging
152
+ response.headers.set('x-request-id', requestId)
153
+ return response
154
+ }
155
+ }
156
+
157
+ // --- Rate limiting middleware ---
158
+ // Simple in-memory rate limiter. Replace with Redis for multi-instance.
159
+ const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
160
+
161
+ export function rateLimit(
162
+ req: NextRequest,
163
+ { limit = 60, windowMs = 60_000 }: { limit?: number; windowMs?: number } = {}
164
+ ): NextResponse | null {
165
+ // Rate limit by IP or user ID (Ahsoka — access control)
166
+ // IP extraction priority: cf-connecting-ip (Cloudflare, set at edge, cannot be spoofed by client)
167
+ // > x-real-ip (nginx) > x-forwarded-for first entry (client-spoofable) > fallback
168
+ const key =
169
+ req.headers.get('x-user-id') ||
170
+ req.headers.get('cf-connecting-ip') ||
171
+ req.headers.get('x-real-ip') ||
172
+ req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
173
+ 'unknown'
174
+ const now = Date.now()
175
+
176
+ const entry = rateLimitMap.get(key)
177
+
178
+ if (!entry || now > entry.resetAt) {
179
+ rateLimitMap.set(key, { count: 1, resetAt: now + windowMs })
180
+ return null // Allowed
181
+ }
182
+
183
+ entry.count++
184
+
185
+ if (entry.count > limit) {
186
+ return NextResponse.json(
187
+ { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
188
+ {
189
+ status: 429,
190
+ headers: {
191
+ 'Retry-After': String(Math.ceil((entry.resetAt - now) / 1000)),
192
+ },
193
+ }
194
+ )
195
+ }
196
+
197
+ return null // Allowed
198
+ }
199
+
200
+ // =============================================================================
201
+ // Cookie Authentication
202
+ // =============================================================================
203
+
204
+ /**
205
+ * 1. Setting HttpOnly Cookies on Login
206
+ *
207
+ * After successful authentication, set the JWT in an HttpOnly cookie.
208
+ * HttpOnly prevents XSS from reading the token. Secure ensures HTTPS only.
209
+ * SameSite=Lax blocks cross-site POST requests while allowing top-level navigation.
210
+ *
211
+ * === Express / Next.js API Route ===
212
+ *
213
+ * // POST /api/auth/login
214
+ * const token = await createSessionToken(user.id)
215
+ * res.setHeader('Set-Cookie', [
216
+ * `session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${7 * 24 * 60 * 60}`,
217
+ * ])
218
+ * // Next.js App Router: use cookies() from 'next/headers'
219
+ * // cookies().set('session', token, {
220
+ * // httpOnly: true, secure: true, sameSite: 'lax',
221
+ * // path: '/', maxAge: 7 * 24 * 60 * 60,
222
+ * // })
223
+ * res.json({ user: { id: user.id, email: user.email } })
224
+ *
225
+ * === Django / FastAPI ===
226
+ *
227
+ * # Django view
228
+ * response = JsonResponse({"user": {"id": user.id}})
229
+ * response.set_cookie("session", token, httponly=True, secure=True,
230
+ * samesite="Lax", max_age=7 * 24 * 60 * 60)
231
+ *
232
+ * # FastAPI endpoint
233
+ * response = JSONResponse({"user": {"id": user.id}})
234
+ * response.set_cookie("session", token, httponly=True, secure=True,
235
+ * samesite="lax", max_age=7 * 24 * 60 * 60)
236
+ */
237
+
238
+ /**
239
+ * 2. Reading Cookies with Bearer Fallback
240
+ *
241
+ * Check cookie first (browser clients), then Authorization header (API clients).
242
+ * This lets the same endpoint serve both contexts without separate auth flows.
243
+ */
244
+ export async function authMiddlewareWithCookieFallback(req: NextRequest) {
245
+ // Cookie takes priority — browser clients send this automatically
246
+ let token = req.cookies.get('session')?.value
247
+
248
+ // Bearer fallback — API clients, mobile apps, server-to-server
249
+ if (!token) {
250
+ const authHeader = req.headers.get('authorization')
251
+ if (authHeader?.startsWith('Bearer ')) {
252
+ token = authHeader.slice(7)
253
+ }
254
+ }
255
+
256
+ if (!token) {
257
+ return NextResponse.json(
258
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
259
+ { status: 401 }
260
+ )
261
+ }
262
+
263
+ try {
264
+ // Fail closed — same principle as authMiddleware (Kenobi)
265
+ const session = await verifySessionToken(token)
266
+ if (!session) {
267
+ return NextResponse.json(
268
+ { error: { code: 'UNAUTHORIZED', message: 'Invalid session' } },
269
+ { status: 401 }
270
+ )
271
+ }
272
+
273
+ const response = NextResponse.next()
274
+ response.headers.set('x-user-id', session.userId)
275
+ response.headers.set('x-request-id', nanoid())
276
+ return response
277
+ } catch {
278
+ return NextResponse.json(
279
+ { error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
280
+ { status: 401 }
281
+ )
282
+ }
283
+ }
284
+
285
+ /**
286
+ * 3. CSRF Protection via Custom Header
287
+ *
288
+ * SameSite=Lax already blocks cross-site POSTs, but defense-in-depth matters (Kenobi).
289
+ * Require X-Requested-With on all mutation endpoints. Browsers enforce CORS preflight
290
+ * on custom headers, so a cross-origin attacker cannot set this without server consent.
291
+ *
292
+ * === Django / FastAPI ===
293
+ *
294
+ * # Django: use built-in CsrfViewMiddleware OR check custom header
295
+ * class CSRFCustomHeaderMiddleware:
296
+ * def __call__(self, request):
297
+ * if request.method in ("POST", "PUT", "DELETE", "PATCH"):
298
+ * if request.headers.get("X-Requested-With") != "XMLHttpRequest":
299
+ * return JsonResponse({"error": "CSRF check failed"}, status=403)
300
+ * return self.get_response(request)
301
+ *
302
+ * # FastAPI: use Depends() for the same check
303
+ * async def require_csrf_header(request: Request):
304
+ * if request.method in ("POST", "PUT", "DELETE", "PATCH"):
305
+ * if request.headers.get("x-requested-with") != "XMLHttpRequest":
306
+ * raise HTTPException(403, "CSRF check failed")
307
+ */
308
+ const MUTATION_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH'])
309
+
310
+ export function csrfProtection(req: NextRequest): NextResponse | null {
311
+ if (!MUTATION_METHODS.has(req.method)) {
312
+ return null // GET/HEAD/OPTIONS — no CSRF risk
313
+ }
314
+
315
+ // Cookie-based auth requires the custom header; Bearer tokens are not vulnerable
316
+ const hasCookie = req.cookies.has('session')
317
+ const hasCustomHeader = req.headers.get('x-requested-with') === 'XMLHttpRequest'
318
+
319
+ if (hasCookie && !hasCustomHeader) {
320
+ return NextResponse.json(
321
+ { error: { code: 'CSRF_FAILED', message: 'Missing X-Requested-With header' } },
322
+ { status: 403 }
323
+ )
324
+ }
325
+
326
+ return null // Allowed
327
+ }
328
+
329
+ /**
330
+ * 4. Frontend Fetch Configuration
331
+ *
332
+ * When using cookie auth, every fetch() call must include credentials: 'include'.
333
+ * Without it, the browser won't send or accept cookies on cross-origin requests.
334
+ * This typed wrapper enforces that and sets the CSRF header automatically.
335
+ */
336
+ type ApiResponse<T> = { data: T } | { error: { code: string; message: string } }
337
+
338
+ async function apiFetch<T>(
339
+ path: string,
340
+ options: RequestInit = {}
341
+ ): Promise<ApiResponse<T>> {
342
+ const response = await fetch(path, {
343
+ ...options,
344
+ credentials: 'include', // Required — sends cookies with every request
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ 'X-Requested-With': 'XMLHttpRequest', // CSRF protection for cookie auth
348
+ ...options.headers,
349
+ },
350
+ })
351
+
352
+ if (!response.ok) {
353
+ const body = await response.json().catch(() => ({}))
354
+ return { error: body.error ?? { code: 'UNKNOWN', message: response.statusText } }
355
+ }
356
+
357
+ return { data: await response.json() }
358
+ }
359
+
360
+ // Usage:
361
+ // const result = await apiFetch<{ user: User }>('/api/users/me')
362
+ // if ('error' in result) { handleError(result.error) }
363
+ // else { renderUser(result.data.user) }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Pattern: React Native Screen with All States
3
+ *
4
+ * Key principles:
5
+ * - Every screen handles: loading, empty, error, and success states (same as web component.tsx)
6
+ * - Safe area insets respected — content never under the notch or home indicator
7
+ * - Navigation follows platform conventions (back swipe iOS, hardware back Android)
8
+ * - Touch targets minimum 44pt (iOS) / 48dp (Android)
9
+ * - Keyboard avoidance on all forms
10
+ * - Supports Dynamic Type / system font scaling
11
+ * - Reduced motion respected via useReducedMotion()
12
+ *
13
+ * Agents: Legolas (code), Samwise-Mobile (a11y), Bilbo (copy), Gimli (performance)
14
+ *
15
+ * Framework adaptations:
16
+ * React Native: This file
17
+ * Flutter: Same 4-state pattern with StatefulWidget, SafeArea, MediaQuery
18
+ * SwiftUI: NavigationStack, .safeAreaInset, @Environment(\.dynamicTypeSize)
19
+ *
20
+ * The framework changes, the principle doesn't:
21
+ * EVERY screen handles loading, empty, error, and success. Safe area is non-negotiable.
22
+ */
23
+
24
+ import React from 'react'
25
+ import {
26
+ View,
27
+ Text,
28
+ FlatList,
29
+ TouchableOpacity,
30
+ ActivityIndicator,
31
+ RefreshControl,
32
+ StyleSheet,
33
+ Platform,
34
+ AccessibilityInfo,
35
+ } from 'react-native'
36
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
37
+ import { useNavigation } from '@react-navigation/native'
38
+
39
+ // --- Types ---
40
+ interface Project {
41
+ id: string
42
+ name: string
43
+ description: string | null
44
+ }
45
+
46
+ interface ProjectListScreenProps {
47
+ projects: Project[]
48
+ isLoading: boolean
49
+ error: string | null
50
+ onRefresh: () => void
51
+ onDelete: (id: string) => void
52
+ }
53
+
54
+ export function ProjectListScreen({
55
+ projects,
56
+ isLoading,
57
+ error,
58
+ onRefresh,
59
+ onDelete,
60
+ }: ProjectListScreenProps) {
61
+ const insets = useSafeAreaInsets()
62
+ const navigation = useNavigation()
63
+
64
+ // Loading state — full-screen spinner with a11y announcement
65
+ if (isLoading && projects.length === 0) {
66
+ return (
67
+ <View style={[styles.center, { paddingTop: insets.top }]} accessibilityRole="progressbar">
68
+ <ActivityIndicator size="large" />
69
+ <Text style={styles.loadingText} accessibilityLiveRegion="polite">
70
+ Loading projects...
71
+ </Text>
72
+ </View>
73
+ )
74
+ }
75
+
76
+ // Error state — actionable, not just "something went wrong"
77
+ if (error) {
78
+ return (
79
+ <View style={[styles.center, { paddingTop: insets.top }]} accessibilityRole="alert">
80
+ <Text style={styles.errorTitle}>Couldn't load your projects</Text>
81
+ <Text style={styles.errorMessage}>{error}</Text>
82
+ <TouchableOpacity
83
+ onPress={onRefresh}
84
+ style={styles.retryButton}
85
+ accessibilityRole="button"
86
+ accessibilityLabel="Retry loading projects"
87
+ >
88
+ <Text style={styles.retryText}>Try Again</Text>
89
+ </TouchableOpacity>
90
+ </View>
91
+ )
92
+ }
93
+
94
+ // Empty state — guide the user
95
+ if (projects.length === 0) {
96
+ return (
97
+ <View style={[styles.center, { paddingTop: insets.top }]}>
98
+ <Text style={styles.emptyTitle}>No projects yet</Text>
99
+ <Text style={styles.emptyMessage}>Create your first project to get started.</Text>
100
+ </View>
101
+ )
102
+ }
103
+
104
+ // Success state — pull-to-refresh list
105
+ return (
106
+ <FlatList
107
+ data={projects}
108
+ keyExtractor={(item) => item.id}
109
+ contentContainerStyle={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
110
+ refreshControl={<RefreshControl refreshing={isLoading} onRefresh={onRefresh} />}
111
+ renderItem={({ item }) => (
112
+ <TouchableOpacity
113
+ style={styles.card}
114
+ onPress={() => navigation.navigate('ProjectDetail', { id: item.id })}
115
+ accessibilityRole="button"
116
+ accessibilityLabel={`Open ${item.name}`}
117
+ // Touch target: minimum 44pt height enforced by card padding
118
+ >
119
+ <Text style={styles.cardTitle}>{item.name}</Text>
120
+ {item.description && <Text style={styles.cardDescription}>{item.description}</Text>}
121
+ </TouchableOpacity>
122
+ )}
123
+ />
124
+ )
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
129
+ loadingText: { marginTop: 12, fontSize: 16, color: '#666' },
130
+ errorTitle: { fontSize: 18, fontWeight: '600', color: '#dc2626' },
131
+ errorMessage: { marginTop: 4, fontSize: 14, color: '#dc2626', textAlign: 'center' },
132
+ retryButton: { marginTop: 16, paddingHorizontal: 20, paddingVertical: 12, backgroundColor: '#dc2626', borderRadius: 8 },
133
+ retryText: { color: '#fff', fontWeight: '600' },
134
+ emptyTitle: { fontSize: 18, fontWeight: '600', color: '#111' },
135
+ emptyMessage: { marginTop: 4, fontSize: 14, color: '#666' },
136
+ card: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#e5e7eb', minHeight: 44 },
137
+ cardTitle: { fontSize: 16, fontWeight: '600', color: '#111' },
138
+ cardDescription: { marginTop: 4, fontSize: 14, color: '#666' },
139
+ })
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Pattern: Offline-First Mobile Service
3
+ *
4
+ * Key principles:
5
+ * - Local-first: reads always hit local storage (SQLite/MMKV), writes queue for sync
6
+ * - Optimistic UI: user sees the change immediately, sync happens in background
7
+ * - Conflict resolution: last-write-wins with timestamp, or manual merge for critical data
8
+ * - Network-aware: queue syncs when connection is available, pauses when offline
9
+ * - Retry with backoff: failed syncs retry 3x with exponential backoff, then queue for later
10
+ *
11
+ * Agents: Stark (service logic), Thor (sync queue), Barton (error handling)
12
+ *
13
+ * Framework adaptations:
14
+ * React Native: This file (SQLite via expo-sqlite or react-native-sqlite-storage, NetInfo)
15
+ * Flutter: sqflite + connectivity_plus, same pattern with Dart async
16
+ * SwiftUI: SwiftData or Core Data + NWPathMonitor, same pattern with async/await
17
+ *
18
+ * The framework changes, the principle doesn't:
19
+ * EVERY offline-capable service has: local store, sync queue, conflict resolution, retry.
20
+ */
21
+
22
+ // --- Types ---
23
+ interface SyncQueueItem {
24
+ id: string
25
+ action: 'create' | 'update' | 'delete'
26
+ entity: string
27
+ payload: Record<string, unknown>
28
+ timestamp: number
29
+ retries: number
30
+ }
31
+
32
+ interface SyncResult {
33
+ success: boolean
34
+ serverVersion?: Record<string, unknown>
35
+ conflict?: boolean
36
+ }
37
+
38
+ // --- Local Storage Layer ---
39
+ // In a real implementation, this would use expo-sqlite, WatermelonDB, or MMKV
40
+
41
+ class LocalStore {
42
+ /** Read from local SQLite — always available, even offline */
43
+ async get<T>(entity: string, id: string): Promise<T | null> {
44
+ // SELECT * FROM {entity} WHERE id = ? — local DB
45
+ return null as T | null // placeholder
46
+ }
47
+
48
+ /** Write to local SQLite + queue for sync */
49
+ async set<T extends { id: string }>(entity: string, item: T): Promise<void> {
50
+ // INSERT OR REPLACE INTO {entity} — local DB
51
+ // Then queue the sync
52
+ await SyncQueue.enqueue({
53
+ id: item.id,
54
+ action: 'update',
55
+ entity,
56
+ payload: item as Record<string, unknown>,
57
+ timestamp: Date.now(),
58
+ retries: 0,
59
+ })
60
+ }
61
+
62
+ /** Delete locally + queue for sync */
63
+ async delete(entity: string, id: string): Promise<void> {
64
+ // DELETE FROM {entity} WHERE id = ? — local DB
65
+ await SyncQueue.enqueue({
66
+ id,
67
+ action: 'delete',
68
+ entity,
69
+ payload: {},
70
+ timestamp: Date.now(),
71
+ retries: 0,
72
+ })
73
+ }
74
+
75
+ /** List all items of an entity — local only, instant */
76
+ async list<T>(entity: string): Promise<T[]> {
77
+ // SELECT * FROM {entity} — local DB
78
+ return [] as T[]
79
+ }
80
+ }
81
+
82
+ // --- Sync Queue ---
83
+ // Persisted to SQLite so it survives app restarts
84
+
85
+ class SyncQueue {
86
+ static async enqueue(item: SyncQueueItem): Promise<void> {
87
+ // INSERT INTO sync_queue — persisted locally
88
+ }
89
+
90
+ static async processQueue(apiClient: ApiClient): Promise<void> {
91
+ // 1. Read all pending items from sync_queue
92
+ // 2. For each item:
93
+ // a. Send to server via apiClient
94
+ // b. If success: remove from queue
95
+ // c. If conflict: resolve (last-write-wins or flag for user)
96
+ // d. If network error: increment retries, backoff
97
+ // e. If retries > 3: move to dead letter (show user "sync failed" badge)
98
+ }
99
+
100
+ static async getPendingCount(): Promise<number> {
101
+ // SELECT COUNT(*) FROM sync_queue — for UI badge
102
+ return 0
103
+ }
104
+ }
105
+
106
+ // --- Network-Aware Sync Manager ---
107
+
108
+ class SyncManager {
109
+ private intervalId: ReturnType<typeof setInterval> | null = null
110
+
111
+ /** Start periodic sync — call on app launch */
112
+ start(apiClient: ApiClient): void {
113
+ // Check connectivity, process queue if online
114
+ // Re-check every 30 seconds
115
+ this.intervalId = setInterval(() => {
116
+ // if (NetInfo.isConnected) SyncQueue.processQueue(apiClient)
117
+ }, 30_000)
118
+ }
119
+
120
+ /** Stop sync — call on app background or logout */
121
+ stop(): void {
122
+ if (this.intervalId) clearInterval(this.intervalId)
123
+ }
124
+
125
+ /** Force sync — call on pull-to-refresh */
126
+ async forceSync(apiClient: ApiClient): Promise<void> {
127
+ await SyncQueue.processQueue(apiClient)
128
+ }
129
+ }
130
+
131
+ // --- Conflict Resolution ---
132
+
133
+ function resolveConflict(local: SyncQueueItem, server: Record<string, unknown>): 'keep-local' | 'keep-server' {
134
+ // Last-write-wins: compare timestamps
135
+ const serverTimestamp = (server as { updatedAt?: number }).updatedAt ?? 0
136
+ return local.timestamp > serverTimestamp ? 'keep-local' : 'keep-server'
137
+
138
+ // For critical data (financial, medical), prefer 'flag-for-user' instead of auto-resolve
139
+ }
140
+
141
+ // --- API Client (placeholder) ---
142
+
143
+ interface ApiClient {
144
+ sync(item: SyncQueueItem): Promise<SyncResult>
145
+ }
146
+
147
+ // --- Usage ---
148
+ // const store = new LocalStore()
149
+ // const syncManager = new SyncManager()
150
+ //
151
+ // // On app launch:
152
+ // syncManager.start(apiClient)
153
+ //
154
+ // // Read (always local — instant, works offline):
155
+ // const projects = await store.list<Project>('projects')
156
+ //
157
+ // // Write (local + queue sync — optimistic, works offline):
158
+ // await store.set('projects', { id: '1', name: 'New Project' })
159
+ //
160
+ // // Pull to refresh (force sync):
161
+ // await syncManager.forceSync(apiClient)
162
+ //
163
+ // // Show sync badge:
164
+ // const pending = await SyncQueue.getPendingCount()
165
+ // if (pending > 0) showBadge(`${pending} changes pending sync`)
166
+
167
+ export { LocalStore, SyncQueue, SyncManager, resolveConflict }