Skip to content

feat(shapeai): stories 1.2 + 2.1 + 2.2 - Auth, Camera, Vision AI Pipeline#643

Closed
ryan09062004-dot wants to merge 31 commits intoSynkraAI:mainfrom
ryan09062004-dot:feat/shapeai-stories-1.2-2.1
Closed

feat(shapeai): stories 1.2 + 2.1 + 2.2 - Auth, Camera, Vision AI Pipeline#643
ryan09062004-dot wants to merge 31 commits intoSynkraAI:mainfrom
ryan09062004-dot:feat/shapeai-stories-1.2-2.1

Conversation

@ryan09062004-dot
Copy link
Copy Markdown

@ryan09062004-dot ryan09062004-dot commented Apr 30, 2026

Summary

  • Story 1.2 - User Authentication: Supabase Auth (email + Apple Sign-In), JWT middleware no API Gateway, tela de Login/Register no mobile (React Native + Expo Router)
  • Story 2.1 - AR Camera Capture: Tela de captura com expo-camera, upload de fotos (front + back) para S3 via API Gateway, criacao de analise no banco, navegacao para loading
  • Story 2.2 - Vision AI Processing Pipeline: Servico Python FastAPI ai-engine, MediaPipe Pose para extracao de landmarks, 8 BodyScores (0-100), relatorio Claude claude-sonnet-4-6 com prompt caching, plano de treino personalizado, conformidade LGPD (fotos deletadas antes do report)

Quality Gates

  • Story 1.2 - QA PASS (Quinn)
  • Story 2.1 - QA PASS (Quinn)
  • Story 2.2 - QA PASS (Quinn)

Test Plan

  • Testes unitarios pytest no ai-engine (pipeline, report generator, S3 service)
  • Testes Jest/RNTL no mobile (loading screen scenarios)
  • Testes Vitest no API Gateway (auth middleware, analysis routes)
  • Verificar lint + typecheck no monorepo
  • Validar conformidade LGPD: fotos deletadas antes da geracao do relatorio Claude

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full mobile app: signup/login/forgot-password, Google/Apple sign‑in, paywall/purchases, camera capture (front/back) with 10MB validation, upload/polling, analysis reports, 4‑week workout plans, history, compare, profile, and push‑notification registration; backend analysis engine and API gateway to support processing and webhooks.
  • Documentation

    • PRD, architecture, project brief, story specs, QA checklist, and example env config added.
  • Tests

    • Extensive unit/integration test suites across mobile UI, services, AI pipeline, and API gateway.
  • Chores

    • .gitignore updates, monorepo/package manifests, and tooling/config files added.

Ryan and others added 2 commits April 30, 2026 01:57
Story 1.2 - User Authentication:
- Supabase Auth with SecureStore adapter (JWT, never AsyncStorage)
- Login screen with Google OAuth (PKCE via expo-web-browser) + Apple Sign In (iOS only)
- signup.tsx, forgot-password.tsx created (were missing from filesystem)
- auth.store.ts: signUp added, AUTH_ERROR_MAP for friendly PT-BR errors
- onAuthStateChange returns subscription cleanup to prevent memory leak
- 8 unit tests: initialize, signIn, signUp, signOut flows

Story 2.1 - AR Camera Capture:
- Two-step capture flow (front + back) with HumanSilhouette overlay
- Back camera fix: facing dynamically set per step (was hardcoded 'front')
- Real file size validation via expo-file-system (10 MB limit enforced)
- startAnalysis → uploadPhoto x2 → triggerProcessing pipeline
- SUBSCRIPTION_REQUIRED error handling → friendly paywall alert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…A PASS]

New service: services/ai-engine (Python/FastAPI)
- mediapipe_processor.py: MediaPipe Pose → normalized landmarks dict
- score_calculator.py: 8 BodyScores (0-100) from landmark geometry
- report_generator.py: Claude claude-sonnet-4-6 + cache_control:ephemeral → ReportSection[] with validation
- plan_generator.py: Claude → 4-week WorkoutPlan with WorkoutWeek validation
- s3_service.py: download + delete_both_photos() for LGPD compliance
- db_service.py: get_analysis, mark_photos_deleted (atomic UPDATE), mark_failed
- routers/analysis.py: POST /analyze — full pipeline, S3 delete before Claude, mark_failed on any error

Migrations & tests:
- 004_workout_plans.sql (no-op; table already in 003)
- 5x pytest suites: score_calculator, mediapipe, s3, report_generator, pipeline
- Jest: analysis.loading.test.tsx (5 scenarios: loading, completed nav, failed, timeout, retry)

LGPD: photos deleted before report generation; photos_deleted_at in atomic UPDATE
Security: x-internal-secret sent on callback; ai-engine behind private network

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

Someone is attempting to deploy a commit to the SINKRA - AIOX Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added area: agents Agent system related area: workflows Workflow system related squad mcp type: test Test coverage and quality area: core Core framework (.aios-core/core/) area: installer Installer and setup (packages/installer/) area: synapse SYNAPSE context engine area: cli CLI tools (bin/, packages/aios-pro-cli/) area: pro Pro features (pro/) area: health-check Health check system area: docs Documentation (docs/) area: devops CI/CD, GitHub Actions (.github/) labels Apr 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new ShapeAI monorepo: documentation and PRD, shared types package, an Expo React Native mobile app (auth, camera, analysis UI, purchases, tests), a Fastify API gateway with DB migrations and S3 presigned flows, and a Python FastAPI AI engine implementing MediaPipe scoring and Claude-backed report/plan generation.

Changes

Repository & Docs / Monorepo

Layer / File(s) Summary
Ignore / Env
.gitignore, shapeai/.env.example
Adjust top-level ignore, add example env for mobile and API.
Docs
docs/qa/QA_FIX_REQUEST.md, docs/shapeai/*, docs/stories/*
Adds PRD, architecture, briefs, stories, QA checklist, and fix-request guidance.
Monorepo tooling
shapeai/package.json, shapeai/tsconfig.json, shapeai/turbo.json
Adds root package.json, TypeScript root config, and Turbo pipeline config.
Shared package
shapeai/packages/shared/package.json, .../src/types/*.ts, .../src/index.ts
New @shapeai/shared package with user/analysis types, helpers, and re-exports.

Mobile app

Layer / File(s) Summary
Scaffold & Config
shapeai/apps/mobile/package.json, app.json, tsconfig.json, jest.config.js, jest.setup.js, __mocks__/*, App.tsx, index.ts
Creates Expo app manifest, entry, TS/Jest configs, and test mocks.
Auth store & clients
src/stores/auth.store.ts, src/services/supabase.client.ts, src/services/api.client.ts
Adds Zustand auth store, Supabase client with secure-store adapter, and centralized API client attaching auth header.
Analysis services
src/services/analysis.service.ts, src/services/profile.service.ts, src/services/notification.service.ts, src/services/purchases.service.ts
Adds analysis lifecycle API calls (start/upload/poll), profile and notification helpers, and RevenueCat wrapper.
Navigation & Screens
app/_layout.tsx, app/(app)/*, app/(auth)/*, app/index.tsx, app/(app)/camera.tsx, .../analysis/*
Implements root/auth layouts, tabs, Home, Camera (2-step capture, size check, upload), History, Paywall, Analysis loading/report/workout, Compare, Profile, and auth screens (login/signup/forgot).
UI components
src/components/* (HumanSilhouette, ReportSectionCard, ExerciseItem, WorkoutDayCard, AnalysisHistoryItem)
Adds camera overlay, report and workout UI components, and history list item.
Tests
apps/mobile/tests/**
Adds comprehensive Jest tests for auth store, camera flows, services, screens, components, and purchase flows.

API Gateway (Fastify)

Layer / File(s) Summary
DB client & migrations
services/api-gateway/src/db/client.ts, .../migrations/*
Adds Postgres pool and migrations for users, profiles, analyses, reports, workout_plans, push_tokens, and notifications flag.
S3 & freemium services
src/services/s3.service.ts, src/services/freemium.service.ts, src/services/subscription.service.ts
Adds presigned upload URL generation, S3 delete/extract helpers, freemium enforcement, and subscription status retrieval.
Auth middleware & routes
src/middleware/auth.ts, src/routes/{analyses,profile,subscription,push-tokens}.ts
Adds JWT auth middleware, analyses endpoints (start/upload URLs, process trigger, polling, compare, internal complete), profile CRUD, subscription webhook/status, and push-token upsert route.
Server & notifications
src/server.ts, src/services/notification.service.ts
New Fastify server entrypoint with rate limiting, cron job and notification service to compute/send Expo push reminders.
Tests & config
tsconfig.json, vitest.config.ts, tests/**
Adds TypeScript config, Vitest config, and numerous route/unit tests for analyses, subscription, notification, and push-token flows.

AI Engine (FastAPI + pipeline)

Layer / File(s) Summary
Entrypoint & deps
services/ai-engine/app/main.py, requirements.txt, pytest.ini
Adds FastAPI app entry, requirements, and pytest config.
S3/DB helpers
app/services/s3_service.py, app/services/db_service.py
Adds S3 download/delete helpers and DB helpers to fetch/mark analyses and mark failures/photos-deleted.
MediaPipe & scoring
app/pipeline/mediapipe_processor.py, app/pipeline/score_calculator.py
Adds image-to-landmarks processor and geometric scoring calculator returning BodyScores.
Report & plan generation
app/pipeline/report_generator.py, app/pipeline/plan_generator.py
Adds Claude-based report and 4-week workout plan generators with strict JSON validation.
Router & pipeline
app/routers/analysis.py
Adds POST /analyze pipeline: download photos, process landmarks, calculate scores, generate report/plan, delete photos, and call API Gateway internal complete endpoint.
Tests
services/ai-engine/tests/**
Adds unit and integration-style pytest suites for mediapipe, score calc, report/plan generation, S3, and pipeline behaviors.

Sequence Diagram

sequenceDiagram
    participant Mobile as Mobile App
    participant Gateway as API Gateway
    participant S3 as AWS S3
    participant Engine as AI Engine
    participant DB as PostgreSQL

    Mobile->>Gateway: POST /analyses (start)
    Gateway->>DB: INSERT analysis (status=processing)
    Gateway->>S3: generate presigned upload URLs
    Gateway-->>Mobile: return analysis_id + upload_urls

    Mobile->>S3: PUT front photo
    Mobile->>S3: PUT back photo
    Mobile->>Gateway: POST /analyses/{id}/process

    Gateway->>Engine: POST /analyze (analysis_id, user_id)

    Engine->>S3: GET front photo
    Engine->>Engine: MediaPipe pose detection
    Engine->>Engine: calculate scores & generate report/workout
    Engine->>S3: DELETE photos
    Engine->>Gateway: POST /internal/analyses/{id}/complete (payload)

    Gateway->>DB: UPDATE analysis (status=completed), UPSERT report/workout
    Mobile->>Gateway: GET /analyses/{id} (polling)
    Gateway->>DB: SELECT analysis + report + plan
    Gateway-->>Mobile: return completed results
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

type: docs

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welcome to aiox-core! Thanks for your first pull request.

What happens next?

  1. Automated checks will run on your PR
  2. A maintainer will review your changes
  3. Once approved, we'll merge your contribution!

PR Checklist:

Thanks for contributing!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟡 Minor comments (9)
docs/stories/2.1.ar-camera-capture.md-7-11 (1)

7-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint violations in code fences and table spacing.

Line 7 and Line 82 fenced blocks are missing language tags, and the table around Line 175 should be surrounded by blank lines to satisfy MD040/MD058.

🛠️ Suggested doc lint fix
-```
+```yaml
 executor: "@dev"
 quality_gate: "@architect"
 quality_gate_tools: ["vitest", "jest"]

- +text
POST /analyses → cria análise + retorna presigned URLs → 201 { analysis_id, upload_urls: { front, back } }
POST /analyses/:id/process → dispara pipeline ai-engine (async) → 202 { status: 'processing' }
GET /analyses/:id → status e resultado → 200 { id, status, scores?, report?, workout_plan? }


## Change Log
+
| Data | Versão | Descrição | Autor |
|------|--------|-----------|-------|
| Abr 2026 | 1.0 | Story criada | River (`@sm`) |
| Abr 2026 | 1.1 | Corrigida para alinhar com arquitetura aprovada: 2 fotos (front+back), endpoint único POST /analyses, schema analyses sem s3_key, sem status pending | Dex (`@dev`) |
| Abr 2026 | 1.2 | Validação `@po`: seção CodeRabbit Integration adicionada, status confirmado Ready — GO ✅ | Pax (`@po`) |
| Abr 2026 | 1.3 | QA CONCERNS fixes: câmera traseira para step 'back', validação real de tamanho com expo-file-system, mock expo-file-system nos testes | Dex (`@dev`) |
+

Also applies to: 82-86, 175-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/stories/2.1.ar-camera-capture.md` around lines 7 - 11, Update the three
Markdown blocks: add language tags to the fenced YAML block beginning with
"executor: \"@dev\"" (use ```yaml) and to the API example block containing the
POST/GET routes (use ```text), and ensure the table under the "Change Log"
header is preceded and followed by a blank line so the table is separated from
surrounding paragraphs; locate these by the YAML block with executor, the fenced
block that starts with "POST /analyses", and the "Change Log" section/table and
apply the language tag and spacing fixes consistently for the other occurrences
referenced (lines ~82-86 and ~175-180).
docs/shapeai/brief-shapeai.md-55-56 (1)

55-56: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align privacy statement with implemented architecture.

Line 55 says processing is on-device, but the implemented flow uploads to backend services for server-side analysis. Please update this wording to avoid privacy/compliance confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/shapeai/brief-shapeai.md` around lines 55 - 56, The phrasing in the
table row with "**Privacy-first** | Processamento on-device para dados
sensíveis" is inaccurate given the implemented flow uploads data to backend
services; update that table cell to accurately state that sensitive data may be
uploaded for server-side analysis (e.g., "Processamento remoto: uploads ao
servidor para análise") or a neutral phrasing like "Dados sensíveis podem ser
enviados ao backend para análise", and ensure the "**Privacy-first**" heading is
adjusted if needed to avoid implying purely on-device processing.
shapeai/apps/mobile/app/(app)/analysis/[id].tsx-14-27 (1)

14-27: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize id before polling.

useLocalSearchParams may provide string | string[]; calling pollAnalysis with an array can break request construction.

Defensive normalization
-  const { id } = useLocalSearchParams<{ id: string }>()
+  const { id } = useLocalSearchParams<{ id?: string | string[] }>()
+  const analysisId = Array.isArray(id) ? id[0] : id
@@
-    if (!id) return
+    if (!analysisId) return
@@
-    pollAnalysis(id)
+    pollAnalysis(analysisId)
@@
-          router.replace(`/(app)/analysis/${id}/report`)
+          router.replace(`/(app)/analysis/${analysisId}/report`)
@@
-  }, [id])
+  }, [analysisId])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/analysis/[id].tsx around lines 14 - 27,
useLocalSearchParams can return string|string[] so passing id directly to
pollAnalysis may pass an array; inside the useEffect before calling pollAnalysis
normalize the id (e.g., derive a single string like const normalizedId =
Array.isArray(id) ? id[0] : id) and use that in the setInterval guard and in
pollAnalysis; update any checks that use id to use normalizedId (referencing
useLocalSearchParams, the id variable, and pollAnalysis) and early-return if
normalizedId is falsy.
docs/stories/1.2.user-authentication.md-7-11 (1)

7-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint warnings in fenced blocks/table formatting.

Add fence languages (e.g., yaml, text) and ensure blank lines around tables to keep docs lint-clean.

Also applies to: 150-162, 177-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/stories/1.2.user-authentication.md` around lines 7 - 11, The fenced code
block containing the keys executor, quality_gate and quality_gate_tools should
include a language tag (e.g., ```yaml) and any other fenced blocks in this file
should get appropriate fence languages; also ensure there is an empty line
before and after any Markdown tables to satisfy markdownlint (apply the same
fixes to the other affected ranges noted: 150-162 and 177-180). Locate the block
with the tokens executor, quality_gate, quality_gate_tools and update the fence
to include the language (yaml or text) and add blank lines around tables
throughout the document.
docs/stories/1.2.user-authentication.md-125-130 (1)

125-130: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove AsyncStorage from the secure-token reference snippet.

The snippet imports @react-native-async-storage/async-storage in a section that explicitly forbids it. This creates copy/paste risk in auth code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/stories/1.2.user-authentication.md` around lines 125 - 130, The snippet
wrongly imports AsyncStorage even though the section mandates using SecureStore;
remove the AsyncStorage import statement and any references to it so the
secure-token example only imports and uses SecureStore (see the import line for
AsyncStorage and the ExpoSecureStoreAdapter symbol) ensuring the example
exports/uses SecureStore exclusively to prevent copy/paste of insecure storage.
docs/shapeai/architecture-shapeai.md-500-510 (1)

500-510: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align documented ai-engine path with implemented structure

The architecture tree documents services/ai-engine/src/..., but this PR’s implementation uses services/ai-engine/app/.... Please align the doc to avoid onboarding/debug friction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/shapeai/architecture-shapeai.md` around lines 500 - 510, The documented
ai-engine directory tree is out of sync: the implementation uses
services/ai-engine/app/... rather than services/ai-engine/src/; update the
architecture-shapeai.md tree so all occurrences of "ai-engine/src/..."
(including referenced subpaths like routers/analysis.py,
pipeline/mediapipe_processor.py, pipeline/score_calculator.py,
pipeline/report_generator.py, pipeline/plan_generator.py, and
privacy/photo_cleaner.py) reflect "ai-engine/app/..." to match the actual
repository layout.
docs/stories/2.2.vision-ai-analysis.md-161-170 (1)

161-170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve migration description conflict for 004_workout_plans.sql

This section shows 004_workout_plans.sql creating workout_plans, while Line 220 states 004 is a no-op because the table already exists in 003. Please keep one canonical version to avoid implementation drift.

Also applies to: 220-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/stories/2.2.vision-ai-analysis.md` around lines 161 - 170, The docs
contain conflicting migration descriptions for migration
"004_workout_plans.sql": one shows creating the workout_plans table (CREATE
TABLE workout_plans ...) while another line (around the reference to migration
004) says it's a no-op because the table already exists from migration 003; pick
a single canonical state and update the doc accordingly — either keep the CREATE
TABLE definition for workout_plans (and remove the "no-op" note) or mark 004 as
a no-op and remove the CREATE TABLE block; update the section that references
migration 004 (Line ~220) to match the chosen canonical version and ensure the
migration name/description and the table name workout_plans/analysis_id are
consistent.
docs/stories/2.2.vision-ai-analysis.md-7-11 (1)

7-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint blockers in fenced code/table sections

Add a language to the fenced block (e.g., yaml) and ensure tables are surrounded by blank lines to avoid markdownlint CI failures.

Also applies to: 206-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/stories/2.2.vision-ai-analysis.md` around lines 7 - 11, The fenced code
block containing the YAML keys (executor: "@dev", quality_gate: "@architect",
quality_gate_tools: ["pytest", "vitest"]) is missing a language tag and causes
markdownlint failures; update that fenced block to start with ```yaml and also
ensure any nearby Markdown tables (notably the table around lines 206-210) are
preceded and followed by a blank line so the linter recognizes them as separate
blocks.
shapeai/services/ai-engine/app/pipeline/score_calculator.py-79-79 (1)

79-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename ambiguous loop variable to satisfy lint and readability

The comprehension uses l as a variable name, which triggers Ruff E741 and may fail lint checks. Rename to explicit names like left_key/right_key.

Suggested change
-    diffs = [abs(_y(landmarks_front, l) - _y(landmarks_front, r)) for l, r in pairs]
+    diffs = [
+        abs(_y(landmarks_front, left_key) - _y(landmarks_front, right_key))
+        for left_key, right_key in pairs
+    ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/ai-engine/app/pipeline/score_calculator.py` at line 79, The
list comprehension assigning diffs uses ambiguous loop variables `l` and `r`;
rename them to descriptive names (e.g., `left_key` and `right_key`) to satisfy
Ruff E741 and improve readability. Update the comprehension: replace `l, r` with
`left_key, right_key` and use those names in the call to `_y(landmarks_front,
...)`, and ensure any other references to `l`/`r` within the same scope
(including nearby comprehensions or loops using `pairs`) are similarly renamed
to avoid shadowing and lint errors.
🧹 Nitpick comments (15)
.gitignore (1)

141-141: 💤 Low value

Redundant negation pattern.

The negation !shapeai/apps/ has no effect because the pattern apps/ on line 140 only ignores a root-level apps/ directory, not shapeai/apps/. Consider removing this line or adding a comment to clarify intent.

♻️ Suggested simplification
-!shapeai/apps/

Or, if documenting intent is desired:

+# shapeai/apps/ is tracked (not affected by apps/ ignore above)
 !shapeai/apps/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 141, The negation pattern '!shapeai/apps/' is redundant
because the existing 'apps/' ignore pattern already does not apply to
'shapeai/apps/', so remove the '!shapeai/apps/' line; if your intent was
actually to unignore a nested directory, instead adjust the ignore rules (for
example modify 'apps/' to a rooted '/apps/' or explicitly ignore 'shapeai/apps/'
and then use '!shapeai/apps/') so the negation has an effect.
shapeai/apps/mobile/tests/analysis/analysis.service.test.ts (2)

1-8: ⚡ Quick win

Use absolute imports in this test module.

Lines 1, 3, and 8 are using relative imports; please convert them to the repo’s absolute import style.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/analysis/analysis.service.test.ts` around lines 1 -
8, Replace the relative imports in this test with the repository’s absolute
import paths: change the import of pollAnalysis from
'../../src/services/analysis.service' to the project's absolute module path for
that service, and change the mocked import and subsequent real import of apiGet
and apiPost from '../../src/services/api.client' to the absolute module path for
the API client; keep the jest.mock wrapper and named imports (pollAnalysis,
apiGet, apiPost) the same so tests and mocks continue to reference the same
symbols.

10-11: Use jest.MockedFunction<typeof ...> to preserve type safety for mocked functions.

Lines 10–11 currently use broad jest.Mock casts, which lose the function signatures for apiGet<T>(path: string): Promise<T> and apiPost<T>(path: string, body?: unknown): Promise<T>. Replace with jest.MockedFunction<typeof apiGet> and jest.MockedFunction<typeof apiPost> to maintain type enforcement for arguments and return types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/analysis/analysis.service.test.ts` around lines 10
- 11, Replace the broad jest.Mock casts for mockGet and mockPost with typed
mocked function types so the original signatures are preserved: change the casts
from jest.Mock to jest.MockedFunction<typeof apiGet> for mockGet and
jest.MockedFunction<typeof apiPost> for mockPost; this ensures apiGet<T>(path:
string): Promise<T> and apiPost<T>(path: string, body?: unknown): Promise<T>
remain type-safe when mocked.
shapeai/apps/mobile/app/(app)/index.tsx (1)

3-3: ⚡ Quick win

Use an absolute import for the auth store.

Line 3 uses a relative path import; please switch it to the project’s absolute import convention.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/index.tsx at line 3, Replace the relative
import of the auth store in index.tsx (import { useAuthStore } from
'../../src/stores/auth.store') with the project's absolute import path (e.g.,
import { useAuthStore } from 'src/stores/auth.store' or whatever root alias your
project uses) so that useAuthStore is imported via the absolute module path
consistent with the codebase convention.
shapeai/apps/mobile/app/index.tsx (1)

2-2: ⚡ Quick win

Replace relative auth-store import with absolute import.

Line 2 currently uses a relative path; align it with the repo’s absolute import rule.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/index.tsx` at line 2, Replace the relative import of
the auth store in index.tsx by changing the import that references
'../src/stores/auth.store' to the project's absolute import path (use the module
path used elsewhere, e.g., 'stores/auth.store' or the repo's configured base
import) so the useAuthStore import uses an absolute module specifier; update the
import statement that references useAuthStore accordingly to match the repo's
absolute-import convention.
shapeai/apps/mobile/app/(app)/_layout.tsx (1)

2-2: ⚡ Quick win

Use absolute import for auth store.

Line 2 currently imports useAuthStore via a relative path. Please convert it to the configured absolute alias.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/_layout.tsx at line 2, Replace the relative
import of useAuthStore with the configured absolute alias import; locate the
import statement that reads "import { useAuthStore } from
'../../src/stores/auth.store'" and change it to the project’s absolute alias
form (e.g., "import { useAuthStore } from 'src/stores/auth.store'" or "import {
useAuthStore } from '@/stores/auth.store'") so the symbol useAuthStore is
imported via the configured absolute path.
shapeai/apps/mobile/app/(auth)/signup.tsx (1)

4-4: ⚡ Quick win

Replace relative store import with project absolute import.

Line 4 uses a relative path (../../src/stores/auth.store). Please switch to the repo’s absolute import alias for consistency.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(auth)/signup.tsx at line 4, In signup.tsx replace
the relative import of useAuthStore ("../../src/stores/auth.store") with the
project's absolute import alias (useAuthStore from the configured absolute path,
e.g. the repo alias for src/stores/auth.store) so the file uses the standard
absolute import style and matches other modules.
shapeai/apps/mobile/src/services/supabase.client.ts (1)

10-21: ⚡ Quick win

Type the Supabase client with the generated DB schema.

Lines 10–21 create an untyped client, which weakens table/column inference and can leak broad any-style typing across auth/data calls.

As per coding guidelines **/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/supabase.client.ts` around lines 10 - 21,
The supabase client is created without the generated DB types, causing weak
typing; import your generated schema type (e.g., Database) and pass it as the
generic to createClient (createClient<Database>(...)) so that the exported
supabase constant is properly typed for table/column inference and auth/data
calls; keep the same config (auth.storage = ExpoSecureStoreAdapter,
autoRefreshToken, persistSession, detectSessionInUrl) and export the typed
supabase variable.
shapeai/apps/mobile/src/services/api.client.ts (2)

18-32: ⚡ Quick win

Harden JSON typing instead of passing through implicit any.

Lines 18 and 32 return res.json() directly as T; Response.json() is untyped (any) and bypasses TS guarantees. Parse as unknown first and validate/narrow per endpoint contract.

As per coding guidelines **/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/api.client.ts` around lines 18 - 32, The
apiPost function currently returns res.json() directly (untyped any) which
bypasses TS guarantees; modify apiPost to first await res.json() as unknown,
then validate/narrow that value to T before returning (use a runtime
validator/schema or explicit type guards per endpoint contract), and throw if
validation fails; update the error-path handling that reads res.json()
similarly; reference apiPost, res.json(), and API_URL when locating the change.

1-1: ⚡ Quick win

Use absolute import for Supabase client.

Line 1 should use the project’s absolute import alias instead of ./supabase.client.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/api.client.ts` at line 1, In api.client.ts
the relative import "import { supabase } from './supabase.client'" must be
changed to use the project's absolute import alias; replace the
'./supabase.client' specifier with the configured absolute path (e.g., the
project's alias root + '/supabase.client') so the import becomes an absolute
import for the supabase client, verify the alias resolves in tsconfig/webpack
and run the linter/build to confirm no import errors.
shapeai/services/ai-engine/tests/test_s3_service.py (1)

31-37: ⚡ Quick win

Strengthen delete assertions to verify bucket and keys.

Line 37 only checking call_count won’t catch wrong key extraction regressions. Assert the expected Bucket/Key call args.

Example assertion upgrade
+from unittest.mock import call
@@
-    assert mock_s3.delete_object.call_count == 2
+    mock_s3.delete_object.assert_has_calls(
+        [
+            call(Bucket="shapeai-photos", Key="uploads/u/a/front.jpg"),
+            call(Bucket="shapeai-photos", Key="uploads/u/a/back.jpg"),
+        ],
+        any_order=False,
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/ai-engine/tests/test_s3_service.py` around lines 31 - 37,
Replace the weak call_count check with assertions that verify the actual S3
delete parameters: inspect mock_s3.delete_object.call_args_list after calling
delete_both_photos and assert each call includes the expected Bucket and Key
values (derived from the input URLs) so you validate correct key extraction;
reference the mock object mock_s3 and the target function delete_both_photos and
assert on mock_s3.delete_object.call_args_list entries for the two expected
{"Bucket": "...", "Key": "..."} dictionaries.
shapeai/apps/mobile/tests/auth/auth.store.test.ts (1)

29-30: ⚡ Quick win

Replace as never casts with concrete typed fixtures/mocks.

as never suppresses useful type checks in a file that should validate TS contracts.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific."

Also applies to: 42-45, 57-58, 68-69, 81-82, 95-96, 107-108, 122-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts` around lines 29 - 30,
Replace the unsafe `as never` casts by creating concrete, properly-typed
fixtures for the mocked session values (e.g. declare `const fakeSession:
Session` or `const fakeSession: Partial<Session>` with the expected shape) and
use those typed variables when calling
`mockAuth.getSession.mockResolvedValue(...)`; update all occurrences referencing
`fakeSession` and similar test fixtures (the lines around the other listed
instances) so the mocks match the real Session type instead of suppressing TS
checks with `as never`.
shapeai/apps/mobile/src/services/analysis.service.ts (1)

12-13: ⚡ Quick win

Replace unknown[] with shared report/workout types

unknown[] weakens type safety on a core API response. Prefer shared contracts (ReportSection[], WorkoutWeek[]) to keep client-server payloads aligned.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/analysis.service.ts` around lines 12 - 13,
The report and workout_plan properties currently use unknown[] which reduces
type safety; replace report?: { highlights: unknown[]; development_areas:
unknown[] } and workout_plan?: { weeks: unknown[] } with concrete shared types
such as report?: { highlights: ReportSection[]; development_areas:
ReportSection[] } and workout_plan?: { weeks: WorkoutWeek[] }, and import or
declare/export ReportSection and WorkoutWeek (or adjust names to existing shared
contracts) so the compiler enforces the API contract.
shapeai/packages/shared/src/types/analysis.types.ts (1)

91-100: ⚡ Quick win

Make MUSCLE_EMOJI key-safe with keyof BodyScores

Record<string, string> allows missing/extra keys silently. Typing it as Record<keyof BodyScores, string> gives compile-time coverage for all score dimensions.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/packages/shared/src/types/analysis.types.ts` around lines 91 - 100,
Change the MUSCLE_EMOJI declaration to be keyed by BodyScores instead of generic
string: replace Record<string, string> with Record<keyof BodyScores, string>
(update any import/definition of BodyScores if needed), then ensure the
MUSCLE_EMOJI object includes an entry for every key in the BodyScores type and
remove any extra keys; fix any resulting type errors by adding missing keys or
aligning names to the BodyScores property names so the compiler enforces full
coverage.
shapeai/services/api-gateway/src/routes/analyses.ts (1)

145-149: ⚡ Quick win

Tighten callback body types instead of unknown[]

unknown[] for report/workout_plan weakens compile-time guarantees. Use shared ReportSection/WorkoutWeek contracts (or local strict interfaces) for safer payload handling.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined. Check for any 'any' type usage that should be more specific. Ensure exports are properly typed."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 145 - 149,
The handler's Body currently uses unknown[] for report
highlights/development_areas and workout_plan weeks; define strict interfaces
(e.g., ReportSection with fields like title:string, content:string | string[],
metadata?:Record<string,unknown> and WorkoutWeek with fields like
weekNumber:number, sessions:Session[] or similar) and replace unknown[] with
ReportSection[] and WorkoutWeek[] in the app.post<{ Params:..., Body:... }>
generic so the route's Body becomes scores:Record<string,number>, report:{
highlights:ReportSection[]; development_areas:ReportSection[] }, workout_plan:{
weeks:WorkoutWeek[] }; update any imports/exports or local type aliases to reuse
these types across the module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 316da79d-139e-4146-9576-fb31cd868c8c

📥 Commits

Reviewing files that changed from the base of the PR and between dbb9e52 and 6760599.

⛔ Files ignored due to path filters (5)
  • shapeai/apps/mobile/assets/adaptive-icon.png is excluded by !**/*.png
  • shapeai/apps/mobile/assets/favicon.png is excluded by !**/*.png
  • shapeai/apps/mobile/assets/icon.png is excluded by !**/*.png
  • shapeai/apps/mobile/assets/splash-icon.png is excluded by !**/*.png
  • shapeai/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (78)
  • .gitignore
  • docs/qa/QA_FIX_REQUEST.md
  • docs/shapeai/architecture-shapeai.md
  • docs/shapeai/brief-shapeai.md
  • docs/shapeai/prd-shapeai.md
  • docs/stories/1.2.user-authentication.md
  • docs/stories/2.1.ar-camera-capture.md
  • docs/stories/2.2.vision-ai-analysis.md
  • shapeai/.env.example
  • shapeai/apps/mobile/.gitignore
  • shapeai/apps/mobile/App.tsx
  • shapeai/apps/mobile/app.json
  • shapeai/apps/mobile/app/(app)/_layout.tsx
  • shapeai/apps/mobile/app/(app)/analysis/[id].tsx
  • shapeai/apps/mobile/app/(app)/camera.tsx
  • shapeai/apps/mobile/app/(app)/history.tsx
  • shapeai/apps/mobile/app/(app)/index.tsx
  • shapeai/apps/mobile/app/(auth)/_layout.tsx
  • shapeai/apps/mobile/app/(auth)/forgot-password.tsx
  • shapeai/apps/mobile/app/(auth)/login.tsx
  • shapeai/apps/mobile/app/(auth)/signup.tsx
  • shapeai/apps/mobile/app/_layout.tsx
  • shapeai/apps/mobile/app/index.tsx
  • shapeai/apps/mobile/index.ts
  • shapeai/apps/mobile/jest.config.js
  • shapeai/apps/mobile/package.json
  • shapeai/apps/mobile/src/components/camera/HumanSilhouette.tsx
  • shapeai/apps/mobile/src/services/analysis.service.ts
  • shapeai/apps/mobile/src/services/api.client.ts
  • shapeai/apps/mobile/src/services/supabase.client.ts
  • shapeai/apps/mobile/src/stores/auth.store.ts
  • shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx
  • shapeai/apps/mobile/tests/analysis/analysis.service.test.ts
  • shapeai/apps/mobile/tests/auth/auth.store.test.ts
  • shapeai/apps/mobile/tests/camera/camera.screen.test.tsx
  • shapeai/apps/mobile/tsconfig.json
  • shapeai/package.json
  • shapeai/packages/shared/package.json
  • shapeai/packages/shared/src/index.ts
  • shapeai/packages/shared/src/types/analysis.types.ts
  • shapeai/packages/shared/src/types/user.types.ts
  • shapeai/services/ai-engine/app/__init__.py
  • shapeai/services/ai-engine/app/main.py
  • shapeai/services/ai-engine/app/pipeline/__init__.py
  • shapeai/services/ai-engine/app/pipeline/mediapipe_processor.py
  • shapeai/services/ai-engine/app/pipeline/plan_generator.py
  • shapeai/services/ai-engine/app/pipeline/report_generator.py
  • shapeai/services/ai-engine/app/pipeline/score_calculator.py
  • shapeai/services/ai-engine/app/routers/__init__.py
  • shapeai/services/ai-engine/app/routers/analysis.py
  • shapeai/services/ai-engine/app/services/__init__.py
  • shapeai/services/ai-engine/app/services/db_service.py
  • shapeai/services/ai-engine/app/services/s3_service.py
  • shapeai/services/ai-engine/pytest.ini
  • shapeai/services/ai-engine/requirements.txt
  • shapeai/services/ai-engine/tests/__init__.py
  • shapeai/services/ai-engine/tests/test_mediapipe_processor.py
  • shapeai/services/ai-engine/tests/test_pipeline.py
  • shapeai/services/ai-engine/tests/test_report_generator.py
  • shapeai/services/ai-engine/tests/test_s3_service.py
  • shapeai/services/ai-engine/tests/test_score_calculator.py
  • shapeai/services/api-gateway/package.json
  • shapeai/services/api-gateway/src/db/client.ts
  • shapeai/services/api-gateway/src/db/migrations/001_users.sql
  • shapeai/services/api-gateway/src/db/migrations/002_analyses.sql
  • shapeai/services/api-gateway/src/db/migrations/003_reports.sql
  • shapeai/services/api-gateway/src/db/migrations/004_workout_plans.sql
  • shapeai/services/api-gateway/src/middleware/auth.ts
  • shapeai/services/api-gateway/src/routes/analyses.ts
  • shapeai/services/api-gateway/src/routes/profile.ts
  • shapeai/services/api-gateway/src/server.ts
  • shapeai/services/api-gateway/src/services/freemium.service.ts
  • shapeai/services/api-gateway/src/services/s3.service.ts
  • shapeai/services/api-gateway/tests/analyses/analyses.routes.test.ts
  • shapeai/services/api-gateway/tsconfig.json
  • shapeai/services/api-gateway/vitest.config.ts
  • shapeai/tsconfig.json
  • shapeai/turbo.json

Comment on lines +14 to +34
"expo": "~54.0.33",
"expo-camera": "~17.0.10",
"expo-file-system": "~19.0.22",
"expo-image-picker": "~17.0.11",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"zustand": "^5.0.12"
},
"devDependencies": {
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^30.0.0",
"@types/react": "~19.1.0",
"jest": "^30.3.0",
"jest-expo": "^55.0.16",
"typescript": "~5.9.2"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For Expo SDK 54, what jest-expo version range is officially supported?

💡 Result:

For Expo SDK 54, the officially supported version range for jest-expo is ^54.0.0 (specifically, versions from 54.0.0 up to the latest patch release 54.0.17 as of the available data). This is determined by running npx expo install jest-expo, which installs the compatible version matching the installed Expo SDK version. Expo SDK packages, including jest-expo, follow semantic versioning aligned with the SDK major version, and patch releases within the 54.x.x range are supported and recommended for bug fixes.

Citations:


Use jest-expo ^54.0.x with Expo SDK 54.

Line 33 pins jest-expo to ^55.0.16, which requires Expo SDK 55. Expo SDK 54 (line 14) requires jest-expo ^54.x.x range. Update jest-expo to ^54.0.0 to match the Expo SDK version and prevent test runner failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/package.json` around lines 14 - 34, The devDependency
"jest-expo" is set to ^55.0.16 while the project uses "expo": "~54.0.33"; change
the "jest-expo" entry in package.json devDependencies from ^55.0.16 to a ^54.0.0
range (e.g., "^54.0.0") so it matches Expo SDK 54, then reinstall dependencies
to apply the change.

Comment on lines +46 to +53
export interface Exercise {
name: string
muscle_group: string
sets: number
reps: string
rest_seconds: number
note: string | null
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Shared Exercise type =="
fd -i 'analysis.types.ts' | xargs -I{} rg -n -C3 'interface Exercise|note|notes' "{}"

echo "== API gateway usage =="
fd -i 'analyses.ts' | xargs -I{} rg -n -C3 'workout|exercise|note|notes' "{}"

echo "== AI engine generators =="
fd -i 'plan_generator.py' | xargs -I{} rg -n -C3 'note|notes|exercise|sessions|weeks' "{}"

echo "== Mobile rendering usage =="
rg -n --type=ts --type=tsx -C3 'exercise|note|notes|rest_seconds' shapeai/apps/mobile || true

Repository: SynkraAI/aiox-core

Length of output: 3270


Reconcile Exercise field naming: note vs notes causing silent data loss

The TypeScript interface defines note: string | null, but the AI engine's SYSTEM_PROMPT in plan_generator.py generates notes?: string (line 15). This mismatch causes the AI-generated notes value to be silently dropped during deserialization into the Exercise type.

Evidence of mismatch

shapeai/packages/shared/src/types/analysis.types.ts (line 52):

note: string | null

plan_generator.py (line 15 in SYSTEM_PROMPT):

"Każdy Exercise: { name: string, sets: number, reps: string, rest_seconds: number, notes?: string }\n"

Choose one convention (recommended: notes to match generator output), then update both the shared type definition and all consuming code (API gateway, mobile rendering).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/packages/shared/src/types/analysis.types.ts` around lines 46 - 53,
The Exercise interface and consumers must use the same field name for freeform
text—rename Exercise.note to Exercise.notes (string | null or optional string)
and update all code that constructs/deserializes Exercises to match;
specifically change the Exercise interface (Exercise) from note to notes, update
the SYSTEM_PROMPT usage in plan_generator.py (SYSTEM_PROMPT) and any places that
parse AI output, API gateway handlers, and mobile/rendering components that read
Exercise.note to read Exercise.notes so the AI-generated notes are preserved
end-to-end.

Comment on lines +42 to +43
[frontResult.url, backResult.url, analysisId]
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generatePresignedUploadUrl implementation =="
fd -i 's3.service.ts' | xargs -I{} rg -n -C4 'generatePresignedUploadUrl|presign|presigned' "{}"

echo "== API route persistence points =="
fd -i 'analyses.ts' | xargs -I{} rg -n -C4 'photo_front_url|photo_back_url|upload_urls|/analyze' "{}"

echo "== ai-engine photo retrieval/deletion behavior =="
fd -i 's3_service.py' | xargs -I{} rg -n -C5 'download|delete|extract_key|photo_front_url|photo_back_url|requests|boto3' "{}"

Repository: SynkraAI/aiox-core

Length of output: 4308


Extend presigned URL TTL or implement URL refresh mechanism

Presigned URLs are generated with only a 5-minute TTL, but the async flow to ai-engine may exceed this. If ai-engine's /analyze processing takes longer than 5 minutes, download_photo() will fail with 403 AccessDenied. Either:

  • Increase PRESIGNED_TTL_SECONDS (line 13, s3.service.ts) to match expected processing time, or
  • Implement URL refresh logic in ai-engine before download, or
  • Pre-download photos in the api-gateway before async notification
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 42 - 43,
Presigned URLs used in the async flow can expire before ai-engine calls
download_photo(), so either increase the TTL or add refresh/download logic:
update PRESIGNED_TTL_SECONDS in s3.service.ts to a value that exceeds the max
ai-engine processing time, or implement URL refresh in ai-engine before invoking
download_photo() (call the API/gateway to request a fresh presigned URL for the
same object), or alternatively have api-gateway pre-download the photos and
include durable storage or the file contents instead of passing
frontResult.url/backResult.url in the analyses route; locate
PRESIGNED_TTL_SECONDS, download_photo(), and the code passing
frontResult.url/backResult.url (analyses.ts) to apply one of these fixes.

Comment on lines +152 to +154
const secret = request.headers['x-internal-secret']
if (secret !== process.env.INTERNAL_SECRET) {
return reply.status(401).send({ error: 'Unauthorized' })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "analyses.ts" -path "*/api-gateway/*" | head -5

Repository: SynkraAI/aiox-core

Length of output: 116


🏁 Script executed:

cat -n shapeai/services/api-gateway/src/routes/analyses.ts | head -160 | tail -30

Repository: SynkraAI/aiox-core

Length of output: 1301


🏁 Script executed:

cat -n shapeai/services/api-gateway/src/routes/analyses.ts | head -20

Repository: SynkraAI/aiox-core

Length of output: 1022


🏁 Script executed:

cat -n shapeai/services/api-gateway/src/routes/analyses.ts | sed -n '1,50p'

Repository: SynkraAI/aiox-core

Length of output: 2032


🏁 Script executed:

cat -n shapeai/services/api-gateway/src/routes/analyses.ts | sed -n '145,160p'

Repository: SynkraAI/aiox-core

Length of output: 754


🏁 Script executed:

wc -l shapeai/services/api-gateway/src/routes/analyses.ts

Repository: SynkraAI/aiox-core

Length of output: 118


🏁 Script executed:

cat shapeai/services/api-gateway/src/routes/analyses.ts

Repository: SynkraAI/aiox-core

Length of output: 6753


🏁 Script executed:

ls -la shapeai/services/api-gateway/src/ | head -20

Repository: SynkraAI/aiox-core

Length of output: 458


🏁 Script executed:

cat shapeai/services/api-gateway/tsconfig.json 2>/dev/null || cat shapeai/services/api-gateway/package.json | head -40

Repository: SynkraAI/aiox-core

Length of output: 440


🏁 Script executed:

#!/bin/bash
FILE="shapeai/services/api-gateway/src/routes/analyses.ts"
echo "=== File: $FILE ==="
cat "$FILE"
echo ""
echo "=== Checking tsconfig.json ==="
if [ -f "shapeai/services/api-gateway/tsconfig.json" ]; then
  cat "shapeai/services/api-gateway/tsconfig.json"
else
  echo "tsconfig.json not found in api-gateway"
fi

Repository: SynkraAI/aiox-core

Length of output: 7229


Harden internal callback authentication to prevent bypass when INTERNAL_SECRET is unset

The authentication check at lines 152-154 has a critical flaw: if process.env.INTERNAL_SECRET is undefined and the request lacks an x-internal-secret header, both values are undefined, making the comparison undefined !== undefined return false, which allows the request to pass. This callback must require explicit secret validation.

Additionally, this file uses relative imports throughout (e.g., ../db/client, ../middleware/auth) which violates the guideline requiring absolute imports in TypeScript files.

Suggested authentication fix
-      const secret = request.headers['x-internal-secret']
-      if (secret !== process.env.INTERNAL_SECRET) {
+      const expectedSecret = process.env.INTERNAL_SECRET
+      if (!expectedSecret) {
+        request.log.error('INTERNAL_SECRET is not configured')
+        return reply.status(500).send({ error: 'Server misconfigured' })
+      }
+      const secret = request.headers['x-internal-secret']
+      if (typeof secret !== 'string' || secret.length === 0 || secret !== expectedSecret) {
         return reply.status(401).send({ error: 'Unauthorized' })
       }

Also convert relative imports to absolute imports (requires configuring baseUrl or paths in tsconfig.json, or using appropriate module aliases).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const secret = request.headers['x-internal-secret']
if (secret !== process.env.INTERNAL_SECRET) {
return reply.status(401).send({ error: 'Unauthorized' })
const expectedSecret = process.env.INTERNAL_SECRET
if (!expectedSecret) {
request.log.error('INTERNAL_SECRET is not configured')
return reply.status(500).send({ error: 'Server misconfigured' })
}
const secret = request.headers['x-internal-secret']
if (typeof secret !== 'string' || secret.length === 0 || secret !== expectedSecret) {
return reply.status(401).send({ error: 'Unauthorized' })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 152 - 154,
The internal callback auth currently compares
request.headers['x-internal-secret'] to process.env.INTERNAL_SECRET and can be
bypassed when INTERNAL_SECRET is unset; update the check in analyses.ts to
require a non-empty configured secret and fail if process.env.INTERNAL_SECRET is
falsy (e.g., throw 500 or 401), then validate that
request.headers['x-internal-secret'] exists and exactly matches the configured
secret before proceeding (use the same header name and symbol names to find the
logic). Also replace all relative imports in this file (e.g., "../db/client",
"../middleware/auth") with the project’s configured absolute import paths (after
ensuring tsconfig baseUrl/paths or aliases are set) so the file uses absolute
imports consistently.

Comment on lines +44 to +53
const fields = Object.keys(updates) as (keyof ProfileBody)[]

if (fields.length === 0) return reply.status(400).send({ error: 'No fields to update' })

const setClauses = fields.map((f, i) => `${f} = $${i + 2}`).join(', ')
const values = [userId, ...fields.map((f) => updates[f])]

const { rows } = await pool.query(
`UPDATE user_profiles SET ${setClauses}, updated_at = NOW()
WHERE user_id = $1 RETURNING *`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

PATCH /profile builds SQL from untrusted keys (injection risk).

Object.keys(request.body) is runtime input. Interpolating keys into SQL allows crafted keys to alter the query. Whitelist allowed columns before building setClauses and reject unknown fields.

Proposed fix
-    const updates = request.body
-    const fields = Object.keys(updates) as (keyof ProfileBody)[]
+    const updates = request.body
+    const allowedFields: (keyof ProfileBody)[] = [
+      'height_cm',
+      'weight_kg',
+      'biological_sex',
+      'primary_goal',
+    ]
+    const rawFields = Object.keys(updates) as string[]
+    const fields = rawFields.filter((f): f is keyof ProfileBody =>
+      allowedFields.includes(f as keyof ProfileBody)
+    )
+
+    if (rawFields.length !== fields.length) {
+      return reply.status(400).send({ error: 'Invalid fields in update payload' })
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 44 - 53, The
code builds SQL SET clauses from untrusted keys (fields / updates / ProfileBody)
which allows injection; before constructing setClauses and values, validate and
whitelist the incoming keys against an allowed-columns list (e.g., const allowed
= ['display_name','bio',...]) and filter fields = fields.filter(f =>
allowed.includes(f as string)); if any input key is not allowed return a 400
error; then build setClauses and values only from the filtered/whitelisted
fields and use parameterized values in pool.query (function/method references:
fields, updates, setClauses, values, pool.query) to eliminate key injection.

Ryan and others added 2 commits April 30, 2026 11:49
- ReportScreen with ScoreGauge (react-native-svg), highlights/development areas (max 3 each), disclaimer, workout navigation
- ReportSectionCard component with muscle emoji map, score badge, highlight/development variants
- getAnalysisResult() service + AnalysisResult/ReportSection types in shared package
- 10/10 tests passing (report-section-card + report.screen suites)
- Fix Jest env: mock expo/src/winter, react singleton via moduleNameMapper (root 19.2.5), react-test-renderer@19.2.5 at workspace root
- Fix analysis.loading.test.tsx: ambiguous regex replaced with exact text query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WorkoutPlanScreen with horizontal week tabs (ScrollView) and vertical day scroll
- WorkoutDayCard component: day label, focus title, exercise list
- ExerciseItem component: name, sets×reps, formatted rest, muscle emoji, note
- profile.service.ts: GET /profile to fetch primary_goal for header
- Header: goal label (PT-BR via GOAL_LABEL) + color-coded score badge + Nova Análise button
- Reuses MUSCLE_EMOJI, formatRest, getScoreColor, calculateOverallScore from @shapeai/shared
- 17 new tests (exercise-item + workout-plan screen); 47/47 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/report.tsx:
- Around line 12-13: Replace the deep relative imports in this file by using
absolute module paths: update the import of getAnalysisResult, AnalysisResult,
BodyScores to import from the absolute module "src/services/analysis.service"
and update ReportSectionCard to import from
"src/components/report/ReportSectionCard"; ensure the import specifiers match
your project's tsconfig/baseUrl or path alias configuration so the symbols get
resolved correctly and run a quick compile to confirm no remaining relative
'../../../../' paths remain.
- Around line 70-72: The effect in useEffect currently returns early when id is
falsy but never clears the loading state, causing an infinite spinner; update
the effect that calls getAnalysisResult(id) to ensure loading is set to false
when id is missing (e.g., call setLoading(false) before the early return or
handle the missing id by redirecting/setting error state), and keep the existing
getAnalysisResult(id) path unchanged so setLoading is still managed after the
fetch.

In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx:
- Around line 18-20: Replace the deep relative imports for getAnalysisResult,
getUserProfile and WorkoutDayCard with the configured absolute import paths;
locate the import lines importing '../../../../src/services/analysis.service',
'../../../../src/services/profile.service', and
'../../../../src/components/workout/WorkoutDayCard' and change them to use
absolute imports (e.g., import { getAnalysisResult } from
'src/services/analysis.service', import { getUserProfile } from
'src/services/profile.service', import WorkoutDayCard from
'src/components/workout/WorkoutDayCard' or whatever project absolute alias is
configured).
- Around line 31-33: The useEffect early-return when id is falsy leaves the
component's loading state true; in the useEffect that references id,
getAnalysisResult and getUserProfile, update the logic so that when id is
missing you explicitly set loading (or an equivalent state like isLoading) to
false (or set an error state) and avoid starting the Promise.all; also ensure
the Promise.all success and failure handlers in the effect set loading to false
and handle errors; locate the useEffect, id variable, and functions
getAnalysisResult/getUserProfile to implement these state updates.
- Line 35: The double-cast on setWeeks masks a real mismatch between the local
AnalysisResult and the shared WorkoutWeek schema; remove the cast and either (A)
update getAnalysisResult/AnalysisResult in analysis.service.ts to import and use
the shared WorkoutWeek/WorkoutSession/Exercise types so workout_plan.weeks
already matches WorkoutWeek[], or (B) map/transform analysis.workout_plan.weeks
before calling setWeeks to produce objects with the expected fields (day, focus,
exercises: Exercise[]) and types; target symbols: setWeeks,
analysis.workout_plan.weeks, getAnalysisResult, AnalysisResult, WorkoutWeek,
WorkoutSession in analysis.service.ts.

In `@shapeai/apps/mobile/src/services/analysis.service.ts`:
- Around line 1-2: The service file imports a UI-layer type ReportSection from a
component (coupling service contract to UI) and uses a relative import; change
the import to use the shared/domain type instead and switch to absolute import
paths. Replace the ReportSection import with the domain/shared type (e.g., a
ReportSection or Report DTO exported from your shared types module) and update
the import to the absolute path configured in tsconfig (instead of
'../components/report/ReportSection'); also ensure apiGet and apiPost imports
remain correct (from './api.client' or switch to absolute if required) so the
analysis.service.ts only depends on domain/shared types, not UI components.
- Around line 13-14: The AnalysisStatusResponse type currently uses unknown[]
for report and workout_plan which loses type safety; define concrete interfaces
(e.g., ReportSection with fields for highlights/development_area items and
WorkoutWeek for week entries) and replace report?: { highlights: unknown[];
development_areas: unknown[] } and workout_plan?: { weeks: unknown[] } with
report?: { highlights: ReportSection[]; development_areas: ReportSection[] } and
workout_plan?: { weeks: WorkoutWeek[] } (or equivalent named types), export
these new interfaces, and scan for any lingering any usages to tighten types
across consumers of AnalysisStatusResponse.
- Around line 30-33: The exported WorkoutSession interface is too simple (name:
string, exercises: string[]) and doesn't match consumers that expect fields like
day and focus and structured exercise objects; update the WorkoutSession type to
include day: string (or enum), focus: string, and exercises: Array of a proper
Exercise type (e.g., Exercise with id/name/reps/sets or the fields used by
consuming code), add/export the Exercise interface, and replace any usages or
casts that assume string[] so callers use the correct typed shape (search for
WorkoutSession, exercises, day, focus in current diffs to locate affected code).

In `@shapeai/apps/mobile/src/services/profile.service.ts`:
- Line 1: Replace the relative import of apiGet in profile.service.ts (import {
apiGet } from './api.client') with the project’s configured absolute alias
import (use the tsconfig/webpack alias for the API client) so that apiGet is
imported via the absolute path; update the import statement to reference the
alias and run type-check/build to ensure the path resolves correctly.

In `@shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx`:
- Around line 9-10: Update the relative imports in the test so they use the
project's absolute import alias instead of "../../..." — specifically change the
jest.mock target '../../src/services/analysis.service' to the absolute path
(e.g. 'src/services/analysis.service' or your repo's alias) and do the same for
the other relative import used in the file (the one referencing the same service
in lines importing pollAnalysis); keep the mocked symbol name pollAnalysis and
ensure Jest still finds the module under the absolute alias.

In `@shapeai/apps/mobile/tests/report/report-section-card.test.tsx`:
- Line 3: Replace the relative import of the ReportSectionCard component with
the project's configured absolute alias; locate the import statement that
references ReportSectionCard and change it to use the absolute path (e.g., the
project's alias for the src/components/report module) so tests import
ReportSectionCard via the absolute import rather than
"../../src/components/report/ReportSectionCard".

In `@shapeai/apps/mobile/tests/report/report.screen.test.tsx`:
- Around line 9-10: Replace the relative import paths in the test so they use
the project's absolute alias imports: update the jest.mock call that references
'../../src/services/analysis.service' to use the absolute alias for the services
folder, and change any other relative imports in this file (the import(s) around
lines that reference getAnalysisResult and the other imports near lines 24-25)
to their corresponding absolute alias paths per the project's tsconfig/jsconfig
aliases; ensure the mocked symbol getAnalysisResult and any test imports still
resolve correctly after switching to aliases.

In `@shapeai/apps/mobile/tests/workout/exercise-item.test.tsx`:
- Line 3: Replace the relative import of the ExerciseItem component in the test
with the app's absolute import alias: change the import of ExerciseItem
(currently '../../src/components/workout/ExerciseItem') to the absolute path
used by the project (e.g., src/components/workout/ExerciseItem or the app alias
configured in tsconfig/webpack) so the test uses the canonical absolute import
style for ExerciseItem.

In `@shapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsx`:
- Around line 9-10: In workout-plan.screen.test.tsx replace all relative import
strings with the project's absolute alias imports: update the jest.mock call
that references getAnalysisResult (currently mocking
'../../src/services/analysis.service') and the other relative imports on the
same file to use the configured absolute aliases (the ones your tsconfig/webpack
uses) so the mock target and any imported modules resolve via absolute paths
instead of '../../...'; ensure the jest.mock target string and any import
statements that referenced '../../...' are updated consistently to the absolute
path form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ba1226f-30fd-4aed-879b-aa5b4421843b

📥 Commits

Reviewing files that changed from the base of the PR and between 6760599 and a23724a.

⛔ Files ignored due to path filters (1)
  • shapeai/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • shapeai/apps/mobile/__mocks__/expo-import-meta-registry.js
  • shapeai/apps/mobile/__mocks__/expo-winter.js
  • shapeai/apps/mobile/app/(app)/analysis/[id]/report.tsx
  • shapeai/apps/mobile/app/(app)/analysis/[id]/workout.tsx
  • shapeai/apps/mobile/jest.config.js
  • shapeai/apps/mobile/jest.setup.js
  • shapeai/apps/mobile/src/components/report/ReportSectionCard.tsx
  • shapeai/apps/mobile/src/components/workout/ExerciseItem.tsx
  • shapeai/apps/mobile/src/components/workout/WorkoutDayCard.tsx
  • shapeai/apps/mobile/src/services/analysis.service.ts
  • shapeai/apps/mobile/src/services/profile.service.ts
  • shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx
  • shapeai/apps/mobile/tests/report/report-section-card.test.tsx
  • shapeai/apps/mobile/tests/report/report.screen.test.tsx
  • shapeai/apps/mobile/tests/workout/exercise-item.test.tsx
  • shapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsx
  • shapeai/package.json
✅ Files skipped from review due to trivial changes (5)
  • shapeai/apps/mobile/mocks/expo-winter.js
  • shapeai/apps/mobile/mocks/expo-import-meta-registry.js
  • shapeai/apps/mobile/src/components/workout/WorkoutDayCard.tsx
  • shapeai/apps/mobile/jest.setup.js
  • shapeai/apps/mobile/src/components/report/ReportSectionCard.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • shapeai/package.json
  • shapeai/apps/mobile/jest.config.js

Comment on lines +12 to +13
import { getAnalysisResult, AnalysisResult, BodyScores } from '../../../../src/services/analysis.service'
import ReportSectionCard from '../../../../src/components/report/ReportSectionCard'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace relative imports with absolute imports.

Line 12 and Line 13 use deep relative paths, which violates the project import rule and makes refactors brittle.

As per coding guidelines, "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/report.tsx around lines 12 - 13,
Replace the deep relative imports in this file by using absolute module paths:
update the import of getAnalysisResult, AnalysisResult, BodyScores to import
from the absolute module "src/services/analysis.service" and update
ReportSectionCard to import from "src/components/report/ReportSectionCard";
ensure the import specifiers match your project's tsconfig/baseUrl or path alias
configuration so the symbols get resolved correctly and run a quick compile to
confirm no remaining relative '../../../../' paths remain.

Comment thread shapeai/apps/mobile/app/(app)/analysis/[id]/report.tsx
Comment on lines +18 to +20
import { getAnalysisResult } from '../../../../src/services/analysis.service'
import { getUserProfile } from '../../../../src/services/profile.service'
import WorkoutDayCard from '../../../../src/components/workout/WorkoutDayCard'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use absolute imports instead of deep relative imports.

Line 18 to Line 20 currently use relative paths and should be switched to the configured absolute import style.

As per coding guidelines, "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx around lines 18 -
20, Replace the deep relative imports for getAnalysisResult, getUserProfile and
WorkoutDayCard with the configured absolute import paths; locate the import
lines importing '../../../../src/services/analysis.service',
'../../../../src/services/profile.service', and
'../../../../src/components/workout/WorkoutDayCard' and change them to use
absolute imports (e.g., import { getAnalysisResult } from
'src/services/analysis.service', import { getUserProfile } from
'src/services/profile.service', import WorkoutDayCard from
'src/components/workout/WorkoutDayCard' or whatever project absolute alias is
configured).

Comment on lines +31 to +33
useEffect(() => {
if (!id) return
Promise.all([getAnalysisResult(id), getUserProfile()])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle missing route id to prevent a stuck loading state.

At Line 32, returning early without updating state can leave loading true forever.

💡 Suggested fix
  useEffect(() => {
-    if (!id) return
+    if (!id) {
+      setError('Plano de treino não disponível.')
+      setLoading(false)
+      return
+    }
     Promise.all([getAnalysisResult(id), getUserProfile()])
       .then(([analysis, profile]) => {
         setWeeks(analysis.workout_plan.weeks as unknown as WorkoutWeek[])
         setOverallScore(calculateOverallScore(analysis.scores))
         setGoal(profile.primary_goal)
       })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!id) return
Promise.all([getAnalysisResult(id), getUserProfile()])
useEffect(() => {
if (!id) {
setError('Plano de treino não disponível.')
setLoading(false)
return
}
Promise.all([getAnalysisResult(id), getUserProfile()])
.then(([analysis, profile]) => {
setWeeks(analysis.workout_plan.weeks as unknown as WorkoutWeek[])
setOverallScore(calculateOverallScore(analysis.scores))
setGoal(profile.primary_goal)
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/analysis/[id]/workout.tsx around lines 31 -
33, The useEffect early-return when id is falsy leaves the component's loading
state true; in the useEffect that references id, getAnalysisResult and
getUserProfile, update the logic so that when id is missing you explicitly set
loading (or an equivalent state like isLoading) to false (or set an error state)
and avoid starting the Promise.all; also ensure the Promise.all success and
failure handlers in the effect set loading to false and handle errors; locate
the useEffect, id variable, and functions getAnalysisResult/getUserProfile to
implement these state updates.

Comment thread shapeai/apps/mobile/app/(app)/analysis/[id]/workout.tsx
Comment on lines +9 to +10
jest.mock('../../src/services/analysis.service', () => ({
pollAnalysis: jest.fn(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Convert relative imports to absolute aliases.

Lines 9-10 and Lines 13-14 use relative paths; please migrate to absolute imports for consistency with repo standards.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

Also applies to: 13-14

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/analysis/analysis.loading.test.tsx` around lines 9
- 10, Update the relative imports in the test so they use the project's absolute
import alias instead of "../../..." — specifically change the jest.mock target
'../../src/services/analysis.service' to the absolute path (e.g.
'src/services/analysis.service' or your repo's alias) and do the same for the
other relative import used in the file (the one referencing the same service in
lines importing pollAnalysis); keep the mocked symbol name pollAnalysis and
ensure Jest still finds the module under the absolute alias.

@@ -0,0 +1,42 @@
import React from 'react'
import { render } from '@testing-library/react-native'
import ReportSectionCard from '../../src/components/report/ReportSectionCard'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace relative component import with absolute alias.

Line 3 should use the configured absolute import path instead of ../../....

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/report/report-section-card.test.tsx` at line 3,
Replace the relative import of the ReportSectionCard component with the
project's configured absolute alias; locate the import statement that references
ReportSectionCard and change it to use the absolute path (e.g., the project's
alias for the src/components/report module) so tests import ReportSectionCard
via the absolute import rather than
"../../src/components/report/ReportSectionCard".

Comment on lines +9 to +10
jest.mock('../../src/services/analysis.service', () => ({
getAnalysisResult: jest.fn(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Switch test module imports to absolute aliases.

Lines 9-10 and Lines 24-25 use ../../... relative paths; please use the project absolute import aliases consistently.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

Also applies to: 24-25

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/report/report.screen.test.tsx` around lines 9 - 10,
Replace the relative import paths in the test so they use the project's absolute
alias imports: update the jest.mock call that references
'../../src/services/analysis.service' to use the absolute alias for the services
folder, and change any other relative imports in this file (the import(s) around
lines that reference getAnalysisResult and the other imports near lines 24-25)
to their corresponding absolute alias paths per the project's tsconfig/jsconfig
aliases; ensure the mocked symbol getAnalysisResult and any test imports still
resolve correctly after switching to aliases.

@@ -0,0 +1,74 @@
import React from 'react'
import { render } from '@testing-library/react-native'
import ExerciseItem from '../../src/components/workout/ExerciseItem'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use absolute import path in this test file.

Line 3 currently uses a relative path (../../...); switch to the app alias import convention.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/workout/exercise-item.test.tsx` at line 3, Replace
the relative import of the ExerciseItem component in the test with the app's
absolute import alias: change the import of ExerciseItem (currently
'../../src/components/workout/ExerciseItem') to the absolute path used by the
project (e.g., src/components/workout/ExerciseItem or the app alias configured
in tsconfig/webpack) so the test uses the canonical absolute import style for
ExerciseItem.

Comment on lines +9 to +10
jest.mock('../../src/services/analysis.service', () => ({
getAnalysisResult: jest.fn(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use absolute imports throughout this test file.

Lines 9-10, 13-14, and 17-19 should use absolute alias paths instead of relative ../../... imports.

As per coding guidelines **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

Also applies to: 13-14, 17-19

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/workout/workout-plan.screen.test.tsx` around lines
9 - 10, In workout-plan.screen.test.tsx replace all relative import strings with
the project's absolute alias imports: update the jest.mock call that references
getAnalysisResult (currently mocking '../../src/services/analysis.service') and
the other relative imports on the same file to use the configured absolute
aliases (the ones your tsconfig/webpack uses) so the mock target and any
imported modules resolve via absolute paths instead of '../../...'; ensure the
jest.mock target string and any import statements that referenced '../../...'
are updated consistently to the absolute path form.

Ryan and others added 3 commits May 1, 2026 13:03
- Paywall screen with monthly/annual plans and RevenueCat integration
- useSubscription hook with post-purchase polling
- purchases.service.ts wrapping RevenueCat SDK
- API Gateway: GET /subscription/status + POST /subscription/webhook
- Camera redirects to paywall on 402 SUBSCRIPTION_REQUIRED
- Home badge Free/Pro reflects live subscription status
- 26 new tests (18 mobile + 8 backend), 65 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HistoryScreen: FlatList com paginação load-more, pull-to-refresh e estado vazio
- AnalysisHistoryItem: data DD/MM/YYYY, badge Atual, top 2 development_areas
- GET /analyses: paginação limit/offset + top_development_areas via LEFT JOIN reports
- AnalysisSummary type adicionado ao shared package
- listAnalyses() com suporte a paginação em analysis.service.ts
- 12 novos testes mobile + 3 backend, 93 total passando

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rors

- PURCHASE_CANCELLED → PURCHASE_CANCELLED_ERROR (matches react-native-purchases types)
- Remove { size: true } from FileSystem.getInfoAsync (not in InfoOptions type)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (13)
shapeai/apps/mobile/tests/history/history.screen.test.tsx (1)

13-19: ⚡ Quick win

Use absolute imports in this test file.

The relative paths in the mock and test imports violate the repo guideline for **/*.{js,jsx,ts,tsx} files. Please switch these to the project’s absolute aliases for consistency.

As per coding guidelines, Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/history/history.screen.test.tsx` around lines 13 -
19, The test uses relative imports for the mocked service and HistoryScreen;
update the jest.mock module string and the import statements to use the
project's absolute import aliases instead of relative paths—replace the
'../../src/services/analysis.service' mock target and the
'../../app/(app)/history' and '../../src/services/analysis.service' import
strings with the repository's absolute module aliases used elsewhere so that
listAnalyses, HistoryScreen and the mocked analysis.service resolve via absolute
imports (keep the existing jest.mock call and the named import listAnalyses
unchanged).
shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx (1)

1-4: ⚡ Quick win

Use absolute imports here as well.

AnalysisHistoryItem is imported through a relative path, which conflicts with the repo guideline for TypeScript/TSX files. Please switch to the appropriate absolute alias.

As per coding guidelines, Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx` around lines
1 - 4, The test imports AnalysisHistoryItem via a relative path; change that to
use the project's TypeScript path alias (use the same absolute import pattern
used elsewhere in the test suite) so the import of AnalysisHistoryItem uses the
repo's absolute alias instead of
'../../src/components/history/AnalysisHistoryItem'; update the import statement
in AnalysisHistoryItem.test.tsx to reference the component by its configured
absolute alias (matching other files) and run the tests to confirm resolution.
shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx (1)

18-30: ⚡ Quick win

Use absolute imports in the new test too.

These mocks and imports still use ../../... paths, which conflicts with the repo's TS/TSX import rule.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx` around
lines 18 - 30, Update the test to use absolute imports instead of relative
paths: change the jest.mock calls that reference
'../../src/services/analysis.service' and
'../../src/components/camera/HumanSilhouette' to their absolute module paths,
and update the import of CameraScreen and startAnalysis to use the same absolute
import roots; keep the mocked symbols startAnalysis, uploadPhoto,
triggerProcessing and the HumanSilhouette mock and ensure CameraScreen import
still refers to the default export from the app camera module.
shapeai/apps/mobile/app/(app)/camera.tsx (1)

1-15: ⚡ Quick win

Use absolute imports here.

This file still uses relative paths for local modules, which conflicts with the repo rule for **/*.{js,jsx,ts,tsx} and makes the new screen inconsistent with the rest of the app.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/camera.tsx around lines 1 - 15, This file uses
relative imports for local modules (e.g., startAnalysis, uploadPhoto,
triggerProcessing from ../../src/services/analysis.service and HumanSilhouette
from ../../src/components/camera/HumanSilhouette); update these to use the
project's absolute import paths (replace ../../src/... with the configured
absolute root paths used across the repo) so the CameraView screen follows the
coding guideline for **/*.{js,jsx,ts,tsx}; ensure you update all occurrences in
this file (startAnalysis, uploadPhoto, triggerProcessing, HumanSilhouette) to
the correct absolute module specifiers and verify TypeScript resolves them.
shapeai/apps/mobile/tests/auth/auth.store.test.ts (1)

1-21: ⚡ Quick win

Use absolute imports in the auth store test.

The test still relies on ../../... paths for app code, which violates the repo’s import rule.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts` around lines 1 - 21,
Replace the relative import paths in the test with the project's absolute-module
imports: update the mocked module imports for purchases.service and
supabase.client and the imports for useAuthStore and supabase to use the repo's
configured absolute aliases (instead of '../../src/services/purchases.service',
'../../src/services/supabase.client', '../../src/stores/auth.store'); ensure the
jest.mock calls and the import statements reference the same absolute module
names so the mocks apply correctly to useAuthStore and supabase in
auth.store.test.ts.
shapeai/apps/mobile/src/stores/auth.store.ts (1)

1-4: ⚡ Quick win

Use absolute imports in the auth store.

This new store still pulls local modules through relative paths, which violates the repo's TS import rule and makes the app code inconsistent.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/stores/auth.store.ts` around lines 1 - 4, The auth
store uses relative imports; update the top-of-file imports in auth.store.ts to
use the project's absolute import paths for the local modules (replace imports
for supabase and purchases.service that currently reference
'../services/supabase.client' and '../services/purchases.service' with their
absolute equivalents) while leaving third-party imports (create from 'zustand'
and Session from '@supabase/supabase-js') unchanged; ensure the named symbols
supabase, purchasesLogIn, and purchasesLogOut are imported via the absolute
paths so the file conforms to the repository TS import rule.
shapeai/apps/mobile/app/_layout.tsx (1)

1-5: ⚡ Quick win

Use absolute imports in the root layout.

This file still uses relative paths for local modules, which breaks the repo’s TS/TSX import convention.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 1 - 5, The root layout is
using relative imports for local modules (useAuthStore, configurePurchases);
change those to absolute module specifiers per project convention (e.g., import
{ useAuthStore } from 'src/stores/auth.store' and import { configurePurchases }
from 'src/services/purchases.service') while leaving external imports (react,
expo-router, expo-status-bar) unchanged so the file uses absolute imports for
local code.
shapeai/apps/mobile/tests/subscription/home.screen.test.tsx (1)

4-17: ⚡ Quick win

Use absolute imports in the home screen test.

This test still uses relative paths for the mocked app modules and the screen under test, which conflicts with the repo import rule.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/subscription/home.screen.test.tsx` around lines 4 -
17, The test uses relative module imports for the HomeScreen and mocked modules;
update the jest.mock and import statements that reference useSubscription,
useAuthStore and HomeScreen to use the project's absolute import aliases instead
of relative paths (e.g., change the mocked module specifiers and the HomeScreen
import to the repo's absolute module names), ensuring the mocked symbols
(useSubscription, useAuthStore, and the exported HomeScreen component) are still
correctly referenced.
shapeai/apps/mobile/app/(app)/paywall.tsx (1)

1-11: 💤 Low value

Switch these module specifiers to absolute imports.

The imports still use ../../... paths.

As per coding guidelines, use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/paywall.tsx around lines 1 - 11, Replace the
relative import specifiers in paywall.tsx with the project's absolute import
paths: change "../../src/services/purchases.service" imports (getOfferings,
purchasePackage, restorePurchases, PURCHASES_ERROR_CODE and PurchasesPackage) to
the absolute module path your project uses for services, and change
"../../src/hooks/useSubscription" to the absolute hooks path so that
useSubscription is imported via the absolute alias; keep the existing symbol
names (getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE,
PurchasesPackage, useSubscription) unchanged so references in the file still
resolve.
shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx (1)

1-24: 💤 Low value

Switch these module specifiers to absolute imports.

The mocks/imports still use ../../... paths.

As per coding guidelines, use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx` around lines 1 -
24, Update the test to use absolute module specifiers: change the jest.mock
calls that reference '../../src/services/purchases.service' and
'../../src/hooks/useSubscription' to use their absolute paths (e.g.,
'src/services/purchases.service' and 'src/hooks/useSubscription'), and update
the PaywallScreen import from '../../app/(app)/paywall' to the absolute import
(e.g., 'app/(app)/paywall'); keep the same mock implementations and the existing
references to router, getOfferings, purchasePackage, restorePurchases,
PURCHASES_ERROR_CODE, and useSubscription so only the module specifier strings
are modified.
shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts (1)

1-9: 💤 Low value

Switch these module specifiers to absolute imports.

The mocks/imports still use ../../... paths.

As per coding guidelines, use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts`
around lines 1 - 9, The test currently uses relative module specifiers for the
DB client mock/imports; update the vi.mock and import statements that reference
'../../src/db/client' to use the project's absolute import path (e.g.,
'src/db/client') so that vi.mock('../../src/db/client', ...) and import { pool }
from '../../src/db/client' become vi.mock('src/db/client', ...) and import {
pool } from 'src/db/client'; ensure the local alias mockPool (const mockPool =
pool as unknown as { query: ReturnType<typeof vi.fn> }) and any references to
pool/vi.mock remain unchanged except for the module specifier.
shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts (1)

1-8: 💤 Low value

Switch these module specifiers to absolute imports.

The mocks/imports still use ../../... paths.

As per coding guidelines, use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts`
around lines 1 - 8, Update the test to use absolute module specifiers instead of
relative paths: replace imports and the vi.mock call that reference
'../../src/db/client' and '../../src/services/subscription.service' with the
project's absolute paths (e.g., 'src/db/client' and
'src/services/subscription.service') so the mocked module and the imported
symbols pool and getSubscriptionStatus are resolved via absolute imports; update
any other '../../...' specifiers in this file to their corresponding absolute
'src/...' equivalents.
shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx (1)

1-30: 💤 Low value

Switch these module specifiers to absolute imports.

The mocks/imports still use ../../... paths.

As per coding guidelines, use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 1 -
30, Update the test to use absolute module specifiers instead of relative paths:
change all mocks and imports that reference
'../../src/services/purchases.service', '../../src/hooks/useSubscription', and
'../../app/(app)/paywall' to their absolute equivalents (e.g.,
'src/services/purchases.service', 'src/hooks/useSubscription', and
'app/(app)/paywall' or your project's absolute module roots). Specifically
update the jest.mock calls and the import statements for PaywallScreen,
purchasePackage/restorePurchases/PURCHASES_ERROR_CODE, and useSubscription so
they import from absolute module names matching the app's module resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shapeai/.env.example`:
- Around line 7-15: The .env example uses S3_BUCKET_NAME but the S3 service
expects AWS_S3_BUCKET (see services/api-gateway/src/services/s3.service.ts),
causing an undefined bucket at runtime; update the .env.example to rename
S3_BUCKET_NAME to AWS_S3_BUCKET (keeping the same value) so the environment
variable matches the symbol referenced by the S3 code, or alternatively change
the s3.service.ts code to read S3_BUCKET_NAME—pick one consistent name and apply
it across the .env.example and the s3.service.ts configuration reference.

In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 12-14: The useEffect currently calls initialize() but ignores its
returned unsubscribe; modify the effect so it returns the cleanup function from
initialize() (i.e., call initialize() and return its result) so the auth
listener is unsubscribed on unmount/remount; update the effect that references
initialize in the dependency array to return the unsubscribe instead of
discarding it.

In `@shapeai/apps/mobile/app/`(app)/paywall.tsx:
- Around line 133-136: The save badge text is hard-coded; compute the discount
from the pricing data returned by getOfferings() and render that instead of
"Economize 37%". Locate the pricing values used to display monthly and annual
plans in paywall.tsx (the code that calls getOfferings() and the variables
holding monthlyPrice and annualPrice or the offering objects) and calculate
percent = Math.round((1 - annualPrice / (monthlyPrice * 12)) * 100); replace the
static Text content inside the saveBadge (styles.saveBadgeText) with a string
built from that percent (e.g., `Economize ${percent}%`), and ensure you guard
against missing/zero prices by falling back to no badge or a safe default.
Ensure formatting and localization as needed when converting the number to a
string.

In `@shapeai/apps/mobile/src/hooks/useSubscription.ts`:
- Around line 24-45: pollUntilPro currently uses setInterval which can overlap
requests and resolves even when status never becomes 'pro'; replace it with a
sequential async loop that awaits apiGet('/subscription/status') each iteration,
calls setSubscription(status), and returns immediately when status.status ===
'pro'; after exhausting options.maxAttempts throw or reject (do not resolve) to
signal timeout; update the pollUntilPro implementation (and its Promise
signature) to be an async function that loops (for or while) with await on
apiGet, uses options.intervalMs via an awaited delay between attempts,
references pollUntilPro, apiGet, and setSubscription, and removes the
setInterval-based clearInterval logic.

In `@shapeai/apps/mobile/src/services/purchases.service.ts`:
- Around line 17-36: The paywall helpers (getOfferings, purchasePackage,
restorePurchases) call the RevenueCat SDK unconditionally and will crash when
EXPO_PUBLIC_REVENUECAT_KEY is missing; add the same early-exit guard used in
purchasesLogIn/purchasesLogOut to each of these functions: check
process.env.EXPO_PUBLIC_REVENUECAT_KEY at the top of getOfferings,
purchasePackage, and restorePurchases and short-circuit when missing—return a
safe no-op result (e.g., an empty/safe PurchasesOfferings for getOfferings and a
resolved no-op or a rejected Promise with a clear, descriptive error for
purchasePackage and restorePurchases) so the app won't blow up when RevenueCat
is disabled.

In `@shapeai/services/api-gateway/src/routes/analyses.ts`:
- Around line 136-137: Validate parsed pagination values from
request.query.limit and request.query.offset before using them: parse with radix
10, check isNaN and that offset >= 0 and limit is between 1 and 50; if invalid
return a 400 Bad Request (e.g., reply.code(400) / throw) or normalize by
clamping (limit = Math.min(Math.max(parsedLimit, 1), 50); offset =
Math.max(parsedOffset, 0)). Update the logic around the limit and offset
variables in analyses.ts so malformed values cannot reach the DB.
- Around line 186-213: The three DB writes (the UPDATE on analyses, INSERT into
reports, and INSERT into workout_plans) must be executed inside a single
transaction so the completion callback is atomic; obtain a client from pool (use
pool.connect()), BEGIN a transaction, run the three queries (the query that
updates analyses status/photo deletion, the INSERT ... ON CONFLICT for reports,
and the INSERT ... ON CONFLICT for workout_plans), then COMMIT; on any error
ROLLBACK and rethrow the error, finally release the client. Locate the calls to
pool.query that run the UPDATE analyses, INSERT INTO reports, and INSERT INTO
workout_plans and replace them with transactional client.query usage and proper
error handling/cleanup.

In `@shapeai/services/api-gateway/src/routes/subscription.ts`:
- Around line 18-28: The webhook handler in the app.post('/subscription/webhook'
...) currently casts request.body to { event: RevenueCatEvent } and destructures
body.event directly, which can throw on malformed payloads; add a runtime
validation (using an existing schema lib like zod or a simple guard) to check
that request.body && request.body.event is an object and that event.type is a
string (one of expected values) and event.expiration_at_ms is either a
number/null/undefined before destructuring or calling new
Date(...).toISOString(); if validation fails return a 400 with a clear error
instead of proceeding to DB logic in this handler (referencing request.body,
RevenueCatEvent, type, expiration_at_ms, and the
app.post('/subscription/webhook' route).

---

Nitpick comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 1-5: The root layout is using relative imports for local modules
(useAuthStore, configurePurchases); change those to absolute module specifiers
per project convention (e.g., import { useAuthStore } from
'src/stores/auth.store' and import { configurePurchases } from
'src/services/purchases.service') while leaving external imports (react,
expo-router, expo-status-bar) unchanged so the file uses absolute imports for
local code.

In `@shapeai/apps/mobile/app/`(app)/camera.tsx:
- Around line 1-15: This file uses relative imports for local modules (e.g.,
startAnalysis, uploadPhoto, triggerProcessing from
../../src/services/analysis.service and HumanSilhouette from
../../src/components/camera/HumanSilhouette); update these to use the project's
absolute import paths (replace ../../src/... with the configured absolute root
paths used across the repo) so the CameraView screen follows the coding
guideline for **/*.{js,jsx,ts,tsx}; ensure you update all occurrences in this
file (startAnalysis, uploadPhoto, triggerProcessing, HumanSilhouette) to the
correct absolute module specifiers and verify TypeScript resolves them.

In `@shapeai/apps/mobile/app/`(app)/paywall.tsx:
- Around line 1-11: Replace the relative import specifiers in paywall.tsx with
the project's absolute import paths: change
"../../src/services/purchases.service" imports (getOfferings, purchasePackage,
restorePurchases, PURCHASES_ERROR_CODE and PurchasesPackage) to the absolute
module path your project uses for services, and change
"../../src/hooks/useSubscription" to the absolute hooks path so that
useSubscription is imported via the absolute alias; keep the existing symbol
names (getOfferings, purchasePackage, restorePurchases, PURCHASES_ERROR_CODE,
PurchasesPackage, useSubscription) unchanged so references in the file still
resolve.

In `@shapeai/apps/mobile/src/stores/auth.store.ts`:
- Around line 1-4: The auth store uses relative imports; update the top-of-file
imports in auth.store.ts to use the project's absolute import paths for the
local modules (replace imports for supabase and purchases.service that currently
reference '../services/supabase.client' and '../services/purchases.service' with
their absolute equivalents) while leaving third-party imports (create from
'zustand' and Session from '@supabase/supabase-js') unchanged; ensure the named
symbols supabase, purchasesLogIn, and purchasesLogOut are imported via the
absolute paths so the file conforms to the repository TS import rule.

In `@shapeai/apps/mobile/tests/auth/auth.store.test.ts`:
- Around line 1-21: Replace the relative import paths in the test with the
project's absolute-module imports: update the mocked module imports for
purchases.service and supabase.client and the imports for useAuthStore and
supabase to use the repo's configured absolute aliases (instead of
'../../src/services/purchases.service', '../../src/services/supabase.client',
'../../src/stores/auth.store'); ensure the jest.mock calls and the import
statements reference the same absolute module names so the mocks apply correctly
to useAuthStore and supabase in auth.store.test.ts.

In `@shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx`:
- Around line 1-4: The test imports AnalysisHistoryItem via a relative path;
change that to use the project's TypeScript path alias (use the same absolute
import pattern used elsewhere in the test suite) so the import of
AnalysisHistoryItem uses the repo's absolute alias instead of
'../../src/components/history/AnalysisHistoryItem'; update the import statement
in AnalysisHistoryItem.test.tsx to reference the component by its configured
absolute alias (matching other files) and run the tests to confirm resolution.

In `@shapeai/apps/mobile/tests/history/history.screen.test.tsx`:
- Around line 13-19: The test uses relative imports for the mocked service and
HistoryScreen; update the jest.mock module string and the import statements to
use the project's absolute import aliases instead of relative paths—replace the
'../../src/services/analysis.service' mock target and the
'../../app/(app)/history' and '../../src/services/analysis.service' import
strings with the repository's absolute module aliases used elsewhere so that
listAnalyses, HistoryScreen and the mocked analysis.service resolve via absolute
imports (keep the existing jest.mock call and the named import listAnalyses
unchanged).

In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Around line 1-30: Update the test to use absolute module specifiers instead of
relative paths: change all mocks and imports that reference
'../../src/services/purchases.service', '../../src/hooks/useSubscription', and
'../../app/(app)/paywall' to their absolute equivalents (e.g.,
'src/services/purchases.service', 'src/hooks/useSubscription', and
'app/(app)/paywall' or your project's absolute module roots). Specifically
update the jest.mock calls and the import statements for PaywallScreen,
purchasePackage/restorePurchases/PURCHASES_ERROR_CODE, and useSubscription so
they import from absolute module names matching the app's module resolution.

In `@shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx`:
- Around line 1-24: Update the test to use absolute module specifiers: change
the jest.mock calls that reference '../../src/services/purchases.service' and
'../../src/hooks/useSubscription' to use their absolute paths (e.g.,
'src/services/purchases.service' and 'src/hooks/useSubscription'), and update
the PaywallScreen import from '../../app/(app)/paywall' to the absolute import
(e.g., 'app/(app)/paywall'); keep the same mock implementations and the existing
references to router, getOfferings, purchasePackage, restorePurchases,
PURCHASES_ERROR_CODE, and useSubscription so only the module specifier strings
are modified.

In `@shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx`:
- Around line 18-30: Update the test to use absolute imports instead of relative
paths: change the jest.mock calls that reference
'../../src/services/analysis.service' and
'../../src/components/camera/HumanSilhouette' to their absolute module paths,
and update the import of CameraScreen and startAnalysis to use the same absolute
import roots; keep the mocked symbols startAnalysis, uploadPhoto,
triggerProcessing and the HumanSilhouette mock and ensure CameraScreen import
still refers to the default export from the app camera module.

In `@shapeai/apps/mobile/tests/subscription/home.screen.test.tsx`:
- Around line 4-17: The test uses relative module imports for the HomeScreen and
mocked modules; update the jest.mock and import statements that reference
useSubscription, useAuthStore and HomeScreen to use the project's absolute
import aliases instead of relative paths (e.g., change the mocked module
specifiers and the HomeScreen import to the repo's absolute module names),
ensuring the mocked symbols (useSubscription, useAuthStore, and the exported
HomeScreen component) are still correctly referenced.

In `@shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts`:
- Around line 1-8: Update the test to use absolute module specifiers instead of
relative paths: replace imports and the vi.mock call that reference
'../../src/db/client' and '../../src/services/subscription.service' with the
project's absolute paths (e.g., 'src/db/client' and
'src/services/subscription.service') so the mocked module and the imported
symbols pool and getSubscriptionStatus are resolved via absolute imports; update
any other '../../...' specifiers in this file to their corresponding absolute
'src/...' equivalents.

In
`@shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts`:
- Around line 1-9: The test currently uses relative module specifiers for the DB
client mock/imports; update the vi.mock and import statements that reference
'../../src/db/client' to use the project's absolute import path (e.g.,
'src/db/client') so that vi.mock('../../src/db/client', ...) and import { pool }
from '../../src/db/client' become vi.mock('src/db/client', ...) and import {
pool } from 'src/db/client'; ensure the local alias mockPool (const mockPool =
pool as unknown as { query: ReturnType<typeof vi.fn> }) and any references to
pool/vi.mock remain unchanged except for the module specifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62a06e8a-6770-46a2-9913-75408e44ee2a

📥 Commits

Reviewing files that changed from the base of the PR and between a23724a and 14bcaf1.

⛔ Files ignored due to path filters (1)
  • shapeai/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • shapeai/.env.example
  • shapeai/apps/mobile/app.json
  • shapeai/apps/mobile/app/(app)/_layout.tsx
  • shapeai/apps/mobile/app/(app)/camera.tsx
  • shapeai/apps/mobile/app/(app)/history.tsx
  • shapeai/apps/mobile/app/(app)/index.tsx
  • shapeai/apps/mobile/app/(app)/paywall.tsx
  • shapeai/apps/mobile/app/_layout.tsx
  • shapeai/apps/mobile/jest.config.js
  • shapeai/apps/mobile/package.json
  • shapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsx
  • shapeai/apps/mobile/src/hooks/useSubscription.ts
  • shapeai/apps/mobile/src/services/analysis.service.ts
  • shapeai/apps/mobile/src/services/purchases.service.ts
  • shapeai/apps/mobile/src/stores/auth.store.ts
  • shapeai/apps/mobile/tests/auth/auth.store.test.ts
  • shapeai/apps/mobile/tests/history/AnalysisHistoryItem.test.tsx
  • shapeai/apps/mobile/tests/history/history.screen.test.tsx
  • shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx
  • shapeai/apps/mobile/tests/paywall/paywall.screen.test.tsx
  • shapeai/apps/mobile/tests/subscription/camera.subscription.test.tsx
  • shapeai/apps/mobile/tests/subscription/home.screen.test.tsx
  • shapeai/apps/mobile/tsconfig.json
  • shapeai/package.json
  • shapeai/packages/shared/src/types/analysis.types.ts
  • shapeai/services/api-gateway/src/routes/analyses.ts
  • shapeai/services/api-gateway/src/routes/subscription.ts
  • shapeai/services/api-gateway/src/server.ts
  • shapeai/services/api-gateway/src/services/subscription.service.ts
  • shapeai/services/api-gateway/tests/analyses/analyses.history.test.ts
  • shapeai/services/api-gateway/tests/subscription/subscription.routes.test.ts
  • shapeai/services/api-gateway/tests/subscription/subscription.webhook.test.ts
✅ Files skipped from review due to trivial changes (6)
  • shapeai/apps/mobile/tsconfig.json
  • shapeai/apps/mobile/jest.config.js
  • shapeai/apps/mobile/package.json
  • shapeai/apps/mobile/app.json
  • shapeai/package.json
  • shapeai/packages/shared/src/types/analysis.types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • shapeai/apps/mobile/app/(app)/index.tsx
  • shapeai/services/api-gateway/src/server.ts
  • shapeai/apps/mobile/app/(app)/history.tsx
  • shapeai/apps/mobile/app/(app)/_layout.tsx
  • shapeai/apps/mobile/src/services/analysis.service.ts

Comment thread shapeai/.env.example
Comment on lines +7 to +15
# API Gateway (services/api-gateway/.env)
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/shapeai
SUPABASE_JWT_SECRET=your-jwt-secret
AWS_ACCESS_KEY_ID=your-aws-key
AWS_SECRET_ACCESS_KEY=your-aws-secret
AWS_REGION=us-east-1
S3_BUCKET_NAME=shapeai-uploads
REVENUECAT_WEBHOOK_SECRET=whsec_xxx
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rename the S3 bucket env var to match the gateway.

shapeai/services/api-gateway/src/services/s3.service.ts reads AWS_S3_BUCKET, but this example publishes S3_BUCKET_NAME. Anyone following this template will end up with an undefined bucket at runtime.

♻️ Proposed fix
- S3_BUCKET_NAME=shapeai-uploads
+ AWS_S3_BUCKET=shapeai-uploads
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# API Gateway (services/api-gateway/.env)
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/shapeai
SUPABASE_JWT_SECRET=your-jwt-secret
AWS_ACCESS_KEY_ID=your-aws-key
AWS_SECRET_ACCESS_KEY=your-aws-secret
AWS_REGION=us-east-1
S3_BUCKET_NAME=shapeai-uploads
REVENUECAT_WEBHOOK_SECRET=whsec_xxx
# API Gateway (services/api-gateway/.env)
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/shapeai
SUPABASE_JWT_SECRET=your-jwt-secret
AWS_ACCESS_KEY_ID=your-aws-key
AWS_SECRET_ACCESS_KEY=your-aws-secret
AWS_REGION=us-east-1
AWS_S3_BUCKET=shapeai-uploads
REVENUECAT_WEBHOOK_SECRET=whsec_xxx
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 9-9: [UnorderedKey] The DATABASE_URL key should go before the PORT key

(UnorderedKey)


[warning] 11-11: [UnorderedKey] The AWS_ACCESS_KEY_ID key should go before the DATABASE_URL key

(UnorderedKey)


[warning] 12-12: [UnorderedKey] The AWS_SECRET_ACCESS_KEY key should go before the DATABASE_URL key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The AWS_REGION key should go before the AWS_SECRET_ACCESS_KEY key

(UnorderedKey)


[warning] 14-14: [UnorderedKey] The S3_BUCKET_NAME key should go before the SUPABASE_JWT_SECRET key

(UnorderedKey)


[warning] 15-15: [UnorderedKey] The REVENUECAT_WEBHOOK_SECRET key should go before the S3_BUCKET_NAME key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/.env.example` around lines 7 - 15, The .env example uses
S3_BUCKET_NAME but the S3 service expects AWS_S3_BUCKET (see
services/api-gateway/src/services/s3.service.ts), causing an undefined bucket at
runtime; update the .env.example to rename S3_BUCKET_NAME to AWS_S3_BUCKET
(keeping the same value) so the environment variable matches the symbol
referenced by the S3 code, or alternatively change the s3.service.ts code to
read S3_BUCKET_NAME—pick one consistent name and apply it across the
.env.example and the s3.service.ts configuration reference.

Comment on lines +12 to +14
useEffect(() => {
initialize()
}, [initialize])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return the cleanup from initialize().

initialize() already returns an unsubscribe function, but the effect discards it, so the auth listener stays alive across remounts/HMR.

♻️ Proposed fix
 useEffect(() => {
-    initialize()
+    return initialize()
   }, [initialize])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
initialize()
}, [initialize])
useEffect(() => {
return initialize()
}, [initialize])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 12 - 14, The useEffect
currently calls initialize() but ignores its returned unsubscribe; modify the
effect so it returns the cleanup function from initialize() (i.e., call
initialize() and return its result) so the auth listener is unsubscribed on
unmount/remount; update the effect that references initialize in the dependency
array to return the unsubscribe instead of discarding it.

Comment on lines +133 to +136
<Text style={styles.priceLabel}>Anual</Text>
<View style={styles.saveBadge}>
<Text style={styles.saveBadgeText}>Economize 37%</Text>
</View>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Derive the savings badge from the loaded pricing.

"Economize 37%" is hard-coded while this screen reads plan prices from getOfferings(). If RevenueCat changes the annual price, the copy becomes inaccurate and the UI will advertise the wrong discount.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/paywall.tsx around lines 133 - 136, The save
badge text is hard-coded; compute the discount from the pricing data returned by
getOfferings() and render that instead of "Economize 37%". Locate the pricing
values used to display monthly and annual plans in paywall.tsx (the code that
calls getOfferings() and the variables holding monthlyPrice and annualPrice or
the offering objects) and calculate percent = Math.round((1 - annualPrice /
(monthlyPrice * 12)) * 100); replace the static Text content inside the
saveBadge (styles.saveBadgeText) with a string built from that percent (e.g.,
`Economize ${percent}%`), and ensure you guard against missing/zero prices by
falling back to no badge or a safe default. Ensure formatting and localization
as needed when converting the number to a string.

Comment on lines +24 to +45
// Polling pós-compra: verifica a cada 2s até status pro ou esgotamento (10s)
const pollUntilPro = useCallback(
(options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> =>
new Promise((resolve) => {
let attempts = 0
const interval = setInterval(async () => {
attempts++
try {
const status = await apiGet<SubscriptionStatus>('/subscription/status')
setSubscription(status)
if (status.status === 'pro' || attempts >= options.maxAttempts) {
clearInterval(interval)
resolve()
}
} catch {
if (attempts >= options.maxAttempts) {
clearInterval(interval)
resolve()
}
}
}, options.intervalMs)
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't resolve polling on timeout.

pollUntilPro() currently resolves after maxAttempts even when the status never becomes pro, so the paywall proceeds as if entitlement was confirmed. The setInterval callback can also overlap requests while one fetch is still in flight. Switch to a sequential loop and reject or throw when the limit is hit.

Suggested shape for the fix
-  const pollUntilPro = useCallback(
-    (options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> =>
-      new Promise((resolve) => {
-        let attempts = 0
-        const interval = setInterval(async () => {
-          attempts++
-          try {
-            const status = await apiGet<SubscriptionStatus>('/subscription/status')
-            setSubscription(status)
-            if (status.status === 'pro' || attempts >= options.maxAttempts) {
-              clearInterval(interval)
-              resolve()
-            }
-          } catch {
-            if (attempts >= options.maxAttempts) {
-              clearInterval(interval)
-              resolve()
-            }
-          }
-        }, options.intervalMs)
-      }),
-    []
-  )
+  const pollUntilPro = useCallback(
+    async (options = { intervalMs: 2000, maxAttempts: 5 }): Promise<void> => {
+      for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) {
+        try {
+          const status = await apiGet<SubscriptionStatus>('/subscription/status')
+          setSubscription(status)
+          if (status.status === 'pro') return
+        } catch {
+          // retry until the attempt budget is exhausted
+        }
+
+        if (attempt < options.maxAttempts) {
+          await new Promise(resolve => setTimeout(resolve, options.intervalMs))
+        }
+      }
+
+      throw new Error('Subscription did not become pro in time')
+    },
+    []
+  )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/hooks/useSubscription.ts` around lines 24 - 45,
pollUntilPro currently uses setInterval which can overlap requests and resolves
even when status never becomes 'pro'; replace it with a sequential async loop
that awaits apiGet('/subscription/status') each iteration, calls
setSubscription(status), and returns immediately when status.status === 'pro';
after exhausting options.maxAttempts throw or reject (do not resolve) to signal
timeout; update the pollUntilPro implementation (and its Promise signature) to
be an async function that loops (for or while) with await on apiGet, uses
options.intervalMs via an awaited delay between attempts, references
pollUntilPro, apiGet, and setSubscription, and removes the setInterval-based
clearInterval logic.

Comment on lines +17 to +36
export async function purchasesLogIn(userId: string): Promise<void> {
if (!process.env.EXPO_PUBLIC_REVENUECAT_KEY) return
await Purchases.logIn(userId)
}

export async function purchasesLogOut(): Promise<void> {
if (!process.env.EXPO_PUBLIC_REVENUECAT_KEY) return
await Purchases.logOut()
}

export async function getOfferings(): Promise<PurchasesOfferings> {
return Purchases.getOfferings()
}

export async function purchasePackage(pkg: PurchasesPackage) {
return Purchases.purchasePackage(pkg)
}

export async function restorePurchases() {
return Purchases.restorePurchases()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the RevenueCat disabled path consistent.

The module opts out of RevenueCat when EXPO_PUBLIC_REVENUECAT_KEY is missing for config/auth sync, but the paywall-facing helpers still call the SDK unconditionally. That means a missing key will still blow up once the app tries to load offerings or complete a purchase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/purchases.service.ts` around lines 17 - 36,
The paywall helpers (getOfferings, purchasePackage, restorePurchases) call the
RevenueCat SDK unconditionally and will crash when EXPO_PUBLIC_REVENUECAT_KEY is
missing; add the same early-exit guard used in purchasesLogIn/purchasesLogOut to
each of these functions: check process.env.EXPO_PUBLIC_REVENUECAT_KEY at the top
of getOfferings, purchasePackage, and restorePurchases and short-circuit when
missing—return a safe no-op result (e.g., an empty/safe PurchasesOfferings for
getOfferings and a resolved no-op or a rejected Promise with a clear,
descriptive error for purchasePackage and restorePurchases) so the app won't
blow up when RevenueCat is disabled.

Comment on lines +36 to +49
it('navega para /(app)/paywall ao receber erro SUBSCRIPTION_REQUIRED', async () => {
mockStart.mockRejectedValueOnce(new Error('SUBSCRIPTION_REQUIRED'))
render(<CameraScreen />)

// Disparamos handleUploadAndProcess diretamente via mock interno
// O teste verifica que o router.push foi configurado corretamente
expect(mockPush).toBeDefined()
})

it('não navega para paywall em erros genéricos', () => {
mockStart.mockRejectedValueOnce(new Error('HTTP 500'))
render(<CameraScreen />)
expect(mockPush).not.toHaveBeenCalledWith('/(app)/paywall')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test never reaches the paywall branch.

Both cases stop after render(<CameraScreen />), so the queued mockRejectedValueOnce never drives startAnalysis and router.push('/(app)/paywall') is never asserted. Simulate the capture/confirm flow or invoke the upload path directly so this branch is covered.

Comment on lines +136 to +137
const limit = Math.min(parseInt(request.query.limit ?? '10', 10), 50)
const offset = parseInt(request.query.offset ?? '0', 10)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate pagination inputs before querying.

parseInt() can yield NaN, so malformed limit/offset values currently flow into LIMIT/OFFSET and can turn a bad request into a DB error. Clamp or reject invalid inputs up front.

Suggested fix
-      const limit = Math.min(parseInt(request.query.limit ?? '10', 10), 50)
-      const offset = parseInt(request.query.offset ?? '0', 10)
+      const parsedLimit = Number.parseInt(request.query.limit ?? '10', 10)
+      const parsedOffset = Number.parseInt(request.query.offset ?? '0', 10)
+      if (Number.isNaN(parsedLimit) || Number.isNaN(parsedOffset) || parsedLimit < 1 || parsedOffset < 0) {
+        return reply.status(400).send({ error: 'Invalid pagination parameters' })
+      }
+      const limit = Math.min(parsedLimit, 50)
+      const offset = parsedOffset
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 136 - 137,
Validate parsed pagination values from request.query.limit and
request.query.offset before using them: parse with radix 10, check isNaN and
that offset >= 0 and limit is between 1 and 50; if invalid return a 400 Bad
Request (e.g., reply.code(400) / throw) or normalize by clamping (limit =
Math.min(Math.max(parsedLimit, 1), 50); offset = Math.max(parsedOffset, 0)).
Update the logic around the limit and offset variables in analyses.ts so
malformed values cannot reach the DB.

Comment on lines +186 to +213
// Atualiza análise com scores e status completed
await pool.query(
`UPDATE analyses
SET status = 'completed', scores = $1, completed_at = NOW(),
photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW()
WHERE id = $2`,
[JSON.stringify(scores), id]
)

// Insere relatório
await pool.query(
`INSERT INTO reports (analysis_id, highlights, development_areas)
VALUES ($1, $2, $3)
ON CONFLICT (analysis_id) DO UPDATE
SET highlights = EXCLUDED.highlights,
development_areas = EXCLUDED.development_areas`,
[id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)]
)

// Insere plano de treino
const weeks = workout_plan.weeks
await pool.query(
`INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (analysis_id) DO UPDATE
SET weeks = EXCLUDED.weeks`,
[id, userId, 4, 4, JSON.stringify(weeks)]
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the completion callback atomic.

analyses, reports, and workout_plans are written independently, so a failure after status = 'completed' or photo deletion can leave the pipeline partially persisted. Wrap these writes in a transaction so the callback is all-or-nothing.

Suggested fix
-      await pool.query(
+      const client = await pool.connect()
+      try {
+        await client.query('BEGIN')
+
+        await client.query(
         `UPDATE analyses
          SET status = 'completed', scores = $1, completed_at = NOW(),
              photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW()
          WHERE id = $2`,
         [JSON.stringify(scores), id]
       )
 
-      // Insere relatório
-      await pool.query(
+        // Insere relatório
+        await client.query(
         `INSERT INTO reports (analysis_id, highlights, development_areas)
          VALUES ($1, $2, $3)
          ON CONFLICT (analysis_id) DO UPDATE
            SET highlights = EXCLUDED.highlights,
                development_areas = EXCLUDED.development_areas`,
         [id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)]
       )
 
-      // Insere plano de treino
-      const weeks = workout_plan.weeks
-      await pool.query(
+        // Insere plano de treino
+        const weeks = workout_plan.weeks
+        await client.query(
         `INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks)
          VALUES ($1, $2, $3, $4, $5)
          ON CONFLICT (analysis_id) DO UPDATE
            SET weeks = EXCLUDED.weeks`,
         [id, userId, 4, 4, JSON.stringify(weeks)]
       )
+
+        await client.query('COMMIT')
+      } catch (error) {
+        await client.query('ROLLBACK')
+        throw error
+      } finally {
+        client.release()
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Atualiza análise com scores e status completed
await pool.query(
`UPDATE analyses
SET status = 'completed', scores = $1, completed_at = NOW(),
photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW()
WHERE id = $2`,
[JSON.stringify(scores), id]
)
// Insere relatório
await pool.query(
`INSERT INTO reports (analysis_id, highlights, development_areas)
VALUES ($1, $2, $3)
ON CONFLICT (analysis_id) DO UPDATE
SET highlights = EXCLUDED.highlights,
development_areas = EXCLUDED.development_areas`,
[id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)]
)
// Insere plano de treino
const weeks = workout_plan.weeks
await pool.query(
`INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (analysis_id) DO UPDATE
SET weeks = EXCLUDED.weeks`,
[id, userId, 4, 4, JSON.stringify(weeks)]
)
const client = await pool.connect()
try {
await client.query('BEGIN')
// Atualiza análise com scores e status completed
await client.query(
`UPDATE analyses
SET status = 'completed', scores = $1, completed_at = NOW(),
photo_front_url = NULL, photo_back_url = NULL, photos_deleted_at = NOW()
WHERE id = $2`,
[JSON.stringify(scores), id]
)
// Insere relatório
await client.query(
`INSERT INTO reports (analysis_id, highlights, development_areas)
VALUES ($1, $2, $3)
ON CONFLICT (analysis_id) DO UPDATE
SET highlights = EXCLUDED.highlights,
development_areas = EXCLUDED.development_areas`,
[id, JSON.stringify(report.highlights), JSON.stringify(report.development_areas)]
)
// Insere plano de treino
const weeks = workout_plan.weeks
await client.query(
`INSERT INTO workout_plans (analysis_id, user_id, duration_weeks, sessions_per_week, weeks)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (analysis_id) DO UPDATE
SET weeks = EXCLUDED.weeks`,
[id, userId, 4, 4, JSON.stringify(weeks)]
)
await client.query('COMMIT')
} catch (error) {
await client.query('ROLLBACK')
throw error
} finally {
client.release()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/analyses.ts` around lines 186 - 213,
The three DB writes (the UPDATE on analyses, INSERT into reports, and INSERT
into workout_plans) must be executed inside a single transaction so the
completion callback is atomic; obtain a client from pool (use pool.connect()),
BEGIN a transaction, run the three queries (the query that updates analyses
status/photo deletion, the INSERT ... ON CONFLICT for reports, and the INSERT
... ON CONFLICT for workout_plans), then COMMIT; on any error ROLLBACK and
rethrow the error, finally release the client. Locate the calls to pool.query
that run the UPDATE analyses, INSERT INTO reports, and INSERT INTO workout_plans
and replace them with transactional client.query usage and proper error
handling/cleanup.

Comment on lines +18 to +28
app.post('/subscription/webhook', async (request, reply) => {
const secret = request.headers['x-revenuecat-secret']
if (!secret || secret !== process.env.REVENUECAT_WEBHOOK_SECRET) {
return reply.status(401).send({ error: 'Unauthorized' })
}

const body = request.body as { event: RevenueCatEvent }
const { type, app_user_id, expiration_at_ms } = body.event

if (type === 'INITIAL_PURCHASE' || type === 'RENEWAL') {
const expiresAt = expiration_at_ms ? new Date(expiration_at_ms).toISOString() : null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the webhook payload before destructuring it.

request.body is only cast, so a malformed RevenueCat call can throw at body.event or toISOString() and surface as a 500 instead of a clean 4xx. Add a runtime schema/guard for event, type, and expiration_at_ms before touching the DB.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/subscription.ts` around lines 18 -
28, The webhook handler in the app.post('/subscription/webhook' ...) currently
casts request.body to { event: RevenueCatEvent } and destructures body.event
directly, which can throw on malformed payloads; add a runtime validation (using
an existing schema lib like zod or a simple guard) to check that request.body &&
request.body.event is an object and that event.type is a string (one of expected
values) and event.expiration_at_ms is either a number/null/undefined before
destructuring or calling new Date(...).toISOString(); if validation fails return
a 400 with a clear error instead of proceeding to DB logic in this handler
(referencing request.body, RevenueCatEvent, type, expiration_at_ms, and the
app.post('/subscription/webhook' route).

Ryan and others added 2 commits May 1, 2026 13:47
- POST /analyses/compare: calls Claude API via axios, returns summary+improvements+needs_attention
- HistoryScreen: selection mode UI (Comparar → select 2 → Ver comparativo → navigate)
- AnalysisHistoryItem: isSelectMode/isSelected/onSelect props with checkbox overlay
- CompareScreen: side-by-side dates+scores, muscle group delta bars (▲▼), O que mudou section
- Share button: Pro users share, Free users get paywall alert
- _layout.tsx: registered compare route with href: null
- 13 new tests (history.compare, compare.screen, analyses.compare) — 106 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ations [QA PASS]

- Mobile: expo-notifications permission request + registerPushToken() service
- Mobile: addNotificationResponseReceivedListener in _layout.tsx → deep link to /(app)/camera
- Mobile: ProfileScreen with notifications toggle → PATCH /profile
- Mobile: apiPatch() added to api.client.ts
- Backend: POST /push-tokens route with upsert (ON CONFLICT token)
- Backend: notification.service.ts — getEligibleUsers() SQL, pickTemplate(), sendReanalysisNotifications()
- Backend: DeviceNotRegistered handling removes expired token from push_tokens table
- Backend: node-cron job at 0 9 * * * + POST /internal/notifications/trigger for manual testing
- Backend: migration 004_notifications.sql — adds notifications_enabled to user_profiles + push_tokens table
- fix(jest): moduleNameMapper react path → ../../node_modules/react (hoisted to monorepo root)
- 16 new tests (94 mobile + 28 backend = 122 total passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (2)
shapeai/services/api-gateway/src/routes/profile.ts (1)

45-54: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Whitelist PATCH fields before generating SQL SET clauses.

Line 45 derives SQL column names from runtime input; that allows crafted keys to alter the query text.

Proposed fix
   const updates = request.body
-  const fields = Object.keys(updates) as (keyof ProfileBody)[]
+  const allowedFields: (keyof ProfileBody)[] = [
+    'height_cm',
+    'weight_kg',
+    'biological_sex',
+    'primary_goal',
+    'notifications_enabled',
+  ]
+  const rawFields = Object.keys(updates) as string[]
+  const fields = rawFields.filter((f): f is keyof ProfileBody =>
+    allowedFields.includes(f as keyof ProfileBody)
+  )
+
+  if (rawFields.length !== fields.length) {
+    return reply.status(400).send({ error: 'Invalid fields in update payload' })
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 45 - 54, The
code builds SQL SET clauses directly from runtime keys (fields from updates)
which allows injection via crafted keys; modify the PATCH handler so you first
define an explicit whitelist of allowed profile columns (e.g., const
ALLOWED_PROFILE_FIELDS = ['display_name','bio','avatar_url', ...] as const),
then filter fields = Object.keys(updates).filter(f =>
ALLOWED_PROFILE_FIELDS.includes(f as any)) as (keyof ProfileBody)[]; if filtered
fields is empty return 400; then build setClauses and values from this filtered
list (use the actual column names from the whitelist if they differ from
property names), and call pool.query with those sanitized setClauses and values
(ensure updated_at is still set and WHERE user_id = $1 remains unchanged).
shapeai/apps/mobile/app/_layout.tsx (1)

14-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return the auth cleanup from the Line 14 effect.

If initialize() returns an unsubscribe/teardown, not returning it leaks listeners across remounts.

Proposed fix
   useEffect(() => {
-    initialize()
+    return initialize()
   }, [initialize])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 14 - 16, The useEffect
currently calls initialize() but does not return its teardown/unsubscribe;
change the effect so it returns the cleanup from initialize: if initialize() is
synchronous and returns a function, simply use useEffect(() => initialize(),
[initialize]); if initialize() is async, call it inside the effect, capture the
returned unsubscribe (e.g. let unsub; initialize().then(fn => { unsub = fn });)
and return a cleanup () => { if (typeof unsub === 'function') unsub(); } so the
listener is properly removed on unmount/remount; reference the useEffect and
initialize symbols when making this change.
🟡 Minor comments (3)
shapeai/apps/mobile/app/(app)/compare.tsx-30-33 (1)

30-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against empty score objects in calcOverall.

When scores is empty, division by zero returns NaN and leaks into UI badges.

Proposed fix
 function calcOverall(scores: Record<string, number>): number {
   const vals = Object.values(scores)
+  if (vals.length === 0) return 0
   return Math.round(vals.reduce((s, v) => s + v, 0) / vals.length)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 30 - 33, calcOverall
currently divides by vals.length without checking for an empty scores object,
which yields NaN; update the calcOverall(scores: Record<string, number>)
function to guard against empty input by checking if
Object.values(scores).length === 0 and return a safe default (e.g., 0) before
performing the reduce/divide/Math.round, so UI badges never receive NaN.
shapeai/apps/mobile/tests/profile/profile.screen.test.tsx-44-48 (1)

44-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Re-query the switch before asserting reverted value.

The test stores toggle once on Line 44 and then checks toggle.props.value after state updates. Re-query inside waitFor to avoid stale-node flakiness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx` around lines 44 -
48, The test is using a stale reference to the switch stored in the variable
`toggle` (from `getByTestId('toggle-notifications')`) before state updates;
re-query the DOM inside the `waitFor` callback instead of using
`toggle.props.value` to avoid flakiness — call
`getByTestId('toggle-notifications')` inside `waitFor` and assert the new
`.props.value` (or prefer toAssert via matcher on the element) to verify the
reverted value after `fireEvent(toggle, 'valueChange', false)`.
shapeai/apps/mobile/tests/compare/compare.screen.test.tsx-72-83 (1)

72-83: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert actual Free vs Pro share behavior, not just button presence.

On Line 72 and Line 77 tests only verify visibility. They won’t catch regressions where both plans execute the same share action. Trigger the share press and assert different outcomes per plan.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx` around lines 72 -
83, Update the two tests in compare.screen.test.tsx to assert behavior instead
of mere visibility: in the Free test (rendering <CompareScreen /> with
mockUseSubscription returning free) call
fireEvent.press(getByTestId('btn-compartilhar')) and assert the Pro alert was
shown (e.g., expect(Alert.alert / mockAlert toHaveBeenCalledWith or whatever
project-mocked alert you use); ensure mockUseSubscription returns the free
subscription for that test). In the Pro test (mockUseSubscription returns {
status: 'pro' }) also fireEvent.press(getByTestId('btn-compartilhar')) and
assert the share flow runs (e.g., expect(yourShareHandler or navigator/share
mock toHaveBeenCalled and that no Pro alert was triggered, or check for presence
of shared UI), keeping the existing getByText('Compartilhar') assertion if
desired; ensure you restore or reconfigure mockUseSubscription per test so each
scenario is isolated.
🧹 Nitpick comments (12)
shapeai/services/api-gateway/src/routes/profile.ts (1)

5-11: ⚡ Quick win

Split create and patch DTOs to match actual route behavior.

ProfileBody requires notifications_enabled, but POST doesn’t consume it. Define separate CreateProfileBody and PatchProfileBody to keep route contracts accurate.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined."

Also applies to: 23-24, 42-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/profile.ts` around lines 5 - 11,
ProfileBody currently mixes POST and PATCH inputs; split it into
CreateProfileBody (used by the POST/create route) and PatchProfileBody (used by
the PATCH/update route). CreateProfileBody should include required fields used
on creation (height_cm, weight_kg, biological_sex, primary_goal) and omit
notifications_enabled if POST doesn't consume it; PatchProfileBody should make
fields optional and include notifications_enabled since updates may set it.
Update any route handlers or references in this file from ProfileBody to the
appropriate new type names (CreateProfileBody for create handler and
PatchProfileBody for patch handler) and adjust imports/exports accordingly.
shapeai/apps/mobile/app/(app)/compare.tsx (1)

83-84: ⚡ Quick win

Remove unsafe unknown casts for analysis scores.

Line 83-84 bypasses type safety. Prefer modeling scores in AnalysisSummary (or a local typed extension) instead of casting through unknown.

As per coding guidelines, "**/*.ts: Verify TypeScript types are properly defined."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 83 - 84, The current
unsafe casts of a1/a2 to unknown to access .scores should be removed by adding a
proper type for analysis scores (e.g., add scores: Record<string, number> to
AnalysisSummary or declare a local interface AnalysisWithScores) and then using
that type where the comparator receives items (replace the unknown casts in the
comparator that defines scores1/scores2). Update the comparator signature or map
input items to the new typed shape (use a single safe "as AnalysisWithScores"
only after defining the interface) so you can access .scores without bypassing
TypeScript safety.
shapeai/apps/mobile/tests/compare/compare.screen.test.tsx (1)

18-20: ⚡ Quick win

Switch Line 18–Line 20 imports to absolute paths.

These relative imports violate the TS/TSX import rule and make cross-package moves harder.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx` around lines 18 -
20, Replace the relative imports on the test file for CompareScreen,
apiGet/apiPost and useSubscription with their absolute module paths; update the
import statements that reference '../../app/(app)/compare',
'../../src/services/api.client', and '../../src/hooks/useSubscription' to use
the project's configured absolute paths (keeping the same imported symbols
CompareScreen, apiGet, apiPost, and useSubscription) so the test follows the
"use absolute imports" guideline.
shapeai/apps/mobile/tests/notifications/notification.service.test.ts (1)

1-1: ⚡ Quick win

Normalize Line 1, Line 12, and Line 17 to absolute module paths.

Please align test imports/mocks with the absolute import convention used for TS files.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

Also applies to: 12-12, 17-17

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/notifications/notification.service.test.ts` at line
1, The test imports use relative paths; update them to absolute module paths to
follow the project convention — replace the relative import for
registerPushToken in notification.service.test.ts and any other relative
imports/mocks referenced around lines 12 and 17 with their corresponding
absolute module paths (e.g., use the package-root alias or tsconfig baseUrl
paths for the service and mock modules), ensuring imports reference the same
exported symbols (registerPushToken, and the mocks) but via absolute paths so
TypeScript resolves them consistently.
shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts (1)

7-7: ⚡ Quick win

Use an absolute import on Line 7.

Please align with the TS import rule used across the repo.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts` at
line 7, Replace the relative import of the DB client with an absolute import:
change "import { pool } from '../../src/db/client'" to use the project's
absolute path (e.g. "import { pool } from 'src/db/client'") so the symbol pool
is imported via the repo's TS absolute import convention used across tests and
source code.
shapeai/apps/mobile/app/_layout.tsx (1)

6-7: ⚡ Quick win

Convert Line 6–Line 7 imports to absolute paths.

Please align with the repository-wide TS import convention.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 6 - 7, The imports in
_layout.tsx use relative paths for useAuthStore and configurePurchases; update
them to the project’s absolute TS import style by replacing
'../src/stores/auth.store' and '../src/services/purchases.service' with their
absolute module paths (e.g., starting with the repo alias such as
'@/stores/auth.store' and '@/services/purchases.service') so that useAuthStore
and configurePurchases are imported via absolute imports consistent with the
codebase convention.
shapeai/apps/mobile/tests/profile/profile.screen.test.tsx (1)

13-14: ⚡ Quick win

Use absolute imports on Line 13 and Line 14.

These relative imports break the repo import convention for TS/TSX code.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx` around lines 13 -
14, Replace the two relative imports at the top of the test that pull in
ProfileScreen and apiGet/apiPatch with project-root absolute imports to follow
the TS/TSX import convention; locate the import statements referencing
'../../app/(app)/profile' (ProfileScreen) and '../../src/services/api.client'
(apiGet, apiPatch) and update them to use the repository's absolute module paths
(e.g., the app module path for ProfileScreen and the src/services api.client
path) so the test uses absolute imports instead of relative paths.
shapeai/apps/mobile/app/(app)/profile.tsx (1)

4-4: ⚡ Quick win

Replace Line 4 with an absolute import.

../../src/services/api.client should follow the repo TS import convention.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/profile.tsx at line 4, The import on line 4
uses a relative path; change it to the repo's absolute import form so the two
symbols (apiGet, apiPatch) come from the absolute module instead of
'../../src/services/api.client'. Open the file containing the import of apiGet
and apiPatch and replace the module specifier with the project's absolute import
(e.g., 'src/services/api.client' per TS import convention), keeping the imported
identifiers unchanged; ensure TypeScript resolves the path via existing tsconfig
paths if required.
shapeai/apps/mobile/src/services/notification.service.ts (1)

3-3: ⚡ Quick win

Use an absolute import on Line 3.

./api.client should follow the repo-wide absolute import convention.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/src/services/notification.service.ts` at line 3, Change
the relative import in notification.service.ts from './api.client' to the repo's
absolute import form used elsewhere (e.g., import { apiPost } from
'services/api.client' or the project's configured path alias), so update the
import statement for apiPost in the notification service to use the project's
absolute path convention and ensure it matches tsconfig/webpack path aliases if
applicable.
shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx (1)

18-18: ⚡ Quick win

Use absolute module paths on Line 18, Line 24, and Line 30.

Please align these test imports/mocks with the TS/TSX absolute import convention.

As per coding guidelines "Use absolute imports instead of relative imports in all code".

Also applies to: 24-24, 30-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx` at
line 18, The tests currently mock/import modules using relative paths (e.g.,
'../../src/stores/auth.store', '../../src/stores/user.store',
'../../src/utils/notifications'); update these to use the project's TypeScript
absolute import convention (replace '../../src/stores/auth.store' with the
absolute path like 'src/stores/auth.store', do the same for the user store and
notifications imports) so the jest.mock and import statements in
notification.handler.test.tsx use absolute module paths consistent with the
TS/TSX config.
shapeai/services/api-gateway/src/routes/push-tokens.ts (1)

2-3: ⚡ Quick win

Use absolute imports for internal modules.

Please replace these relative imports with the project’s absolute import style.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/routes/push-tokens.ts` around lines 2 - 3,
The imports in this file use relative paths; replace the two relative imports
("import { pool } from '../db/client'" and "import { requireAuth } from
'../middleware/auth'") with the project's absolute import style (use the
configured path alias used across the repo) so they resolve via the project's
tsconfig/module-alias setup; update the import specifiers for pool and
requireAuth accordingly and verify the build/tsconfig path mapping supports the
chosen absolute base.
shapeai/apps/mobile/app/(app)/index.tsx (1)

3-4: ⚡ Quick win

Use absolute imports instead of relative imports.

Please switch these imports to your configured absolute alias/path style to keep module resolution consistent across the app.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/index.tsx around lines 3 - 4, The imports for
useAuthStore and useSubscription are using relative paths; update them to use
the project's configured absolute import alias (the alias set in your
tsconfig/jsconfig, e.g. "@/..." or "src/...") so module resolution is
consistent. Specifically, replace the relative import lines that reference
'../../src/stores/auth.store' and '../../src/hooks/useSubscription' with their
equivalent absolute-alias imports so useAuthStore and useSubscription are
imported via the app's absolute path scheme.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 14-23: The startup flow calls initialize() but never invokes
registerPushToken(), so device tokens are never upserted; update the layout
startup/auth-ready flow to call registerPushToken() after initialize() (or once
auth is confirmed) and handle/await errors; specifically, inside the same
useEffect that calls initialize() (or a new effect that depends on
initialize/auth-ready state) call registerPushToken() and log or surface
failures, ensuring you don’t duplicate calls and that existing
Notifications.addNotificationResponseReceivedListener behavior is preserved.
- Around line 19-21: The notification handler registered with
Notifications.addNotificationResponseReceivedListener ignores the response and
always routes to '/(app)/camera'; update the listener callback to read the
response parameter (the NotificationResponse) and extract the deep-link route
from response.notification.request.content.data (or the relevant payload key),
then call router.push with that route (falling back to '/(app)/camera' if the
payload route is missing). Ensure you reference the existing listener setup
(Notifications.addNotificationResponseReceivedListener) and the router.push call
when implementing the change.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 4-5: Update the two relative imports in compare.tsx to use
absolute paths: replace the import of apiGet and apiPost from
'../../src/services/api.client' with the absolute module path that exports
apiGet and apiPost (referenced symbol names: apiGet, apiPost) and replace the
import of useSubscription from '../../src/hooks/useSubscription' with its
absolute path (symbol: useSubscription); keep the imported symbol names
unchanged so existing usages in the file still resolve.
- Around line 47-62: The useEffect chain currently sets setIsLoading(false) only
on successful apiGet calls so a failure leaves isLoading true; update the
Promise.all(...) call to ensure setIsLoading(false) runs on both success and
failure (e.g., add a .catch(err => { setResult(null); throw err }) before
continuing or move setIsLoading(false) into a .finally() directly after
Promise.all), and ensure setIsComparing is cleared in all paths (use .finally(()
=> setIsComparing(false)) for the compare request and also call
setIsComparing(false) if Promise.all fails) so that setIsLoading and
setIsComparing are always reset when apiGet or apiPost fail; refer to useEffect,
apiGet, apiPost('/analyses/compare'), setA1, setA2, setResult, setIsLoading, and
setIsComparing to locate the changes.
- Around line 36-50: Validate runtime types/values of id1 and id2 (from
useLocalSearchParams) before using them in API calls: inside the useEffect (and
before any apiPost payload construction), add a guard that rejects undefined or
array values (e.g., !id1 || !id2 || Array.isArray(id1) || Array.isArray(id2)),
handle the error by setting isLoading/isComparing appropriately and returning
early (or navigate/redirect), and only call
apiGet(`/analyses/${id1}`)/apiGet(`/analyses/${id2}`) and any apiPost that uses
id1/id2 after the guard passes so the requests never contain "undefined" or
comma-separated arrays.

In `@shapeai/apps/mobile/src/services/api.client.ts`:
- Line 3: The code currently silently falls back to 'http://localhost:3000' when
process.env.EXPO_PUBLIC_API_URL is missing; change the API_URL initialization so
it fails fast: if process.env.EXPO_PUBLIC_API_URL is undefined and the app is
not running in explicit dev mode (e.g., check __DEV__ or a specific DEV flag),
throw an error explaining the missing EXPO_PUBLIC_API_URL; only allow the
localhost fallback when explicitly running in local development (guarded by
__DEV__ or process.env.NODE_ENV === 'development') and reference the API_URL
constant to locate where to implement this check.
- Line 1: The import in api.client.ts uses a relative path ("./supabase.client")
— replace it with an absolute import for the supabase client (e.g.,
"services/supabase.client" or your project's configured alias) so the line
importing the supabase symbol uses the absolute module path; update the import
that brings in supabase and verify your tsconfig/webpack path aliases are
configured so the new absolute path resolves.
- Around line 18-19: apiGet, apiPost, and apiPatch call res.json() unguarded
which throws on 204 No Content or non-JSON responses; update each function
(apiGet, apiPost, apiPatch) to first check if res.status === 204 or the
Content-Type header does not include "application/json" (or if
res.headers.get("content-length") === "0") and return null (or an appropriate
empty value) in that case, otherwise call and return await res.json(); ensure
you still handle non-ok responses before parsing so error paths remain
unchanged.

In `@shapeai/apps/mobile/tests/history/history.compare.test.tsx`:
- Around line 12-18: Replace the relative imports in this test with the
project's absolute import paths: update the jest.mock target
'../../src/services/analysis.service' to the absolute module path used by the
app (the module that exports listAnalyses), change the import of HistoryScreen
from '../../app/(app)/history' to the app's absolute screen path, and adjust the
import of listAnalyses and any other relative imports (e.g.,
'../../src/services/analysis.service') to their corresponding absolute module
specifiers so the test uses the same absolute module resolution as the
app/router; keep the same symbols (HistoryScreen, router, listAnalyses) and
ensure jest.mock references the absolute service module name.
- Line 38: The test currently calls fireEvent.press inside waitFor (using
waitFor(() => fireEvent.press(getByTestId('btn-comparar')))), which causes
repeated presses; instead use waitFor with a query/assert (e.g., waitFor(() =>
expect(getByTestId('btn-comparar')).toBeTruthy() or getByTestId('btn-comparar')
returns) to wait for the element, then call
fireEvent.press(getByTestId('btn-comparar')) once outside the waitFor; apply
this same change to the other occurrences that currently wrap fireEvent.press
with waitFor (lines referencing waitFor, fireEvent.press, and getByTestId).

In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 2-3: The imports in profile.ts use relative paths; change them to
the project's absolute import aliases (e.g., replace "import { pool } from
'../db/client'" and "import { requireAuth } from '../middleware/auth'" with the
corresponding absolute imports such as "import { pool } from 'db/client'" and
"import { requireAuth } from 'middleware/auth'"), and if necessary ensure the
project's tsconfig/webpack path alias is used so the module resolver recognizes
'db/client' and 'middleware/auth'.

In `@shapeai/services/api-gateway/src/routes/push-tokens.ts`:
- Around line 11-17: The route handler in push-tokens.ts uses relative imports
and lacks runtime schema validation for request.body, so change the imports
(e.g., the client and requireAuth) to absolute TypeScript module paths, and add
a Fastify route schema on the app.post call (use the route options object with
schema.body) that defines required properties token:string and platform: enum
['ios','android']; validate the body via Fastify before you destructure
request.body in the handler (remove or replace the ad-hoc presence check), and
keep using the existing symbols app.post, PushTokenBody (for typing only),
requireAuth, request.body and reply so the handler inserts only validated
platform values into the DB.

In `@shapeai/services/api-gateway/src/services/notification.service.ts`:
- Line 2: The import in notification.service.ts currently pulls the `pool`
symbol using a relative path; change that import to use the project's configured
absolute import (use the tsconfig/baseUrl or path mapping) so `pool` is imported
via the absolute module name instead of '../db/client', update the import
statement that references `pool` accordingly, and ensure any build/tsconfig path
aliases are respected so the service still compiles and tests pass.
- Around line 49-61: The per-user push send currently calls
axios.post(EXPO_PUSH_URL, ...) without a timeout and swallows all errors; update
the axios.post in notification.service.ts to include a reasonable timeout option
(e.g., 3–10s) and change the catch to capture the error object and log a concise
contextual message including the user.token and error details (error.message
and, if present, error.response.data) while still not throwing so the batch
continues; keep the existing DeviceNotRegistered check and pool.query deletion
logic intact.

In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts`:
- Around line 3-11: The test uses a relative module path in the vi.mock call and
the import of pool (the string argument to vi.mock(...) and the import { pool }
statement) — update both to use the project's absolute-import pattern (replace
the relative '../../src/db/client' module specifiers with the repo's configured
absolute module path for the same module), keeping the mock shape (vi.mock(...,
{ pool: { query: vi.fn() } })) and import name unchanged so tests still
reference the same pool symbol.
- Around line 20-41: The helper simulateCompare duplicates route logic and omits
required request details; replace its usage in tests by calling the real POST
/analyses/compare route via fastify.inject so the actual handler executes.
Construct the injected request to include the same payload and headers the route
expects: set method POST, url '/analyses/compare', JSON body with model, system
and user messages (mirror the route's structured system prompt and messages),
include max_tokens: 1024, and set required headers like 'x-api-key' and
'anthropic-version'; use mockPool to prepare DB rows as before and assert
response status (e.g. reply.status === 403) and parsed JSON instead of parsing
mockAxios.raw content. Ensure you remove or stop using simulateCompare (and its
mockAxios call) and reference the actual route's handler behavior in your
assertions.

In
`@shapeai/services/api-gateway/tests/notifications/notification.service.test.ts`:
- Around line 3-14: The test currently imports/mocks internal modules using
relative paths ('../../src/db/client' and
'../../src/services/notification.service'); change these to absolute internal
module paths (e.g. 'src/db/client' and 'src/services/notification.service') in
both the vi.mock(...) and import lines so mocks and imports reference the same
absolute modules (retain the axios mock as-is), ensuring symbols like pool,
getEligibleUsers, pickTemplate, and sendReanalysisNotifications are imported
from the absolute paths.

In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`:
- Around line 11-20: The tests are bypassing the real route by calling the
helper simulatePostPushToken which re-implements route logic; replace usages of
simulatePostPushToken and direct mockPool.query assertions with server-level
requests against the actual POST /push-tokens endpoint (e.g., use app.inject or
fastify.inject against your real app instance), supplying authentication headers
and JSON body to exercise route auth, schema, and handler wiring, then assert
response status/body and verify DB effects (mockPool or test DB) afterwards;
remove or deprecate the simulatePostPushToken helper to ensure future
regressions in the POST /push-tokens route are caught.

---

Minor comments:
In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 30-33: calcOverall currently divides by vals.length without
checking for an empty scores object, which yields NaN; update the
calcOverall(scores: Record<string, number>) function to guard against empty
input by checking if Object.values(scores).length === 0 and return a safe
default (e.g., 0) before performing the reduce/divide/Math.round, so UI badges
never receive NaN.

In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx`:
- Around line 72-83: Update the two tests in compare.screen.test.tsx to assert
behavior instead of mere visibility: in the Free test (rendering <CompareScreen
/> with mockUseSubscription returning free) call
fireEvent.press(getByTestId('btn-compartilhar')) and assert the Pro alert was
shown (e.g., expect(Alert.alert / mockAlert toHaveBeenCalledWith or whatever
project-mocked alert you use); ensure mockUseSubscription returns the free
subscription for that test). In the Pro test (mockUseSubscription returns {
status: 'pro' }) also fireEvent.press(getByTestId('btn-compartilhar')) and
assert the share flow runs (e.g., expect(yourShareHandler or navigator/share
mock toHaveBeenCalled and that no Pro alert was triggered, or check for presence
of shared UI), keeping the existing getByText('Compartilhar') assertion if
desired; ensure you restore or reconfigure mockUseSubscription per test so each
scenario is isolated.

In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx`:
- Around line 44-48: The test is using a stale reference to the switch stored in
the variable `toggle` (from `getByTestId('toggle-notifications')`) before state
updates; re-query the DOM inside the `waitFor` callback instead of using
`toggle.props.value` to avoid flakiness — call
`getByTestId('toggle-notifications')` inside `waitFor` and assert the new
`.props.value` (or prefer toAssert via matcher on the element) to verify the
reverted value after `fireEvent(toggle, 'valueChange', false)`.

---

Duplicate comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 14-16: The useEffect currently calls initialize() but does not
return its teardown/unsubscribe; change the effect so it returns the cleanup
from initialize: if initialize() is synchronous and returns a function, simply
use useEffect(() => initialize(), [initialize]); if initialize() is async, call
it inside the effect, capture the returned unsubscribe (e.g. let unsub;
initialize().then(fn => { unsub = fn });) and return a cleanup () => { if
(typeof unsub === 'function') unsub(); } so the listener is properly removed on
unmount/remount; reference the useEffect and initialize symbols when making this
change.

In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 45-54: The code builds SQL SET clauses directly from runtime keys
(fields from updates) which allows injection via crafted keys; modify the PATCH
handler so you first define an explicit whitelist of allowed profile columns
(e.g., const ALLOWED_PROFILE_FIELDS = ['display_name','bio','avatar_url', ...]
as const), then filter fields = Object.keys(updates).filter(f =>
ALLOWED_PROFILE_FIELDS.includes(f as any)) as (keyof ProfileBody)[]; if filtered
fields is empty return 400; then build setClauses and values from this filtered
list (use the actual column names from the whitelist if they differ from
property names), and call pool.query with those sanitized setClauses and values
(ensure updated_at is still set and WHERE user_id = $1 remains unchanged).

---

Nitpick comments:
In `@shapeai/apps/mobile/app/_layout.tsx`:
- Around line 6-7: The imports in _layout.tsx use relative paths for
useAuthStore and configurePurchases; update them to the project’s absolute TS
import style by replacing '../src/stores/auth.store' and
'../src/services/purchases.service' with their absolute module paths (e.g.,
starting with the repo alias such as '@/stores/auth.store' and
'@/services/purchases.service') so that useAuthStore and configurePurchases are
imported via absolute imports consistent with the codebase convention.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx:
- Around line 83-84: The current unsafe casts of a1/a2 to unknown to access
.scores should be removed by adding a proper type for analysis scores (e.g., add
scores: Record<string, number> to AnalysisSummary or declare a local interface
AnalysisWithScores) and then using that type where the comparator receives items
(replace the unknown casts in the comparator that defines scores1/scores2).
Update the comparator signature or map input items to the new typed shape (use a
single safe "as AnalysisWithScores" only after defining the interface) so you
can access .scores without bypassing TypeScript safety.

In `@shapeai/apps/mobile/app/`(app)/index.tsx:
- Around line 3-4: The imports for useAuthStore and useSubscription are using
relative paths; update them to use the project's configured absolute import
alias (the alias set in your tsconfig/jsconfig, e.g. "@/..." or "src/...") so
module resolution is consistent. Specifically, replace the relative import lines
that reference '../../src/stores/auth.store' and
'../../src/hooks/useSubscription' with their equivalent absolute-alias imports
so useAuthStore and useSubscription are imported via the app's absolute path
scheme.

In `@shapeai/apps/mobile/app/`(app)/profile.tsx:
- Line 4: The import on line 4 uses a relative path; change it to the repo's
absolute import form so the two symbols (apiGet, apiPatch) come from the
absolute module instead of '../../src/services/api.client'. Open the file
containing the import of apiGet and apiPatch and replace the module specifier
with the project's absolute import (e.g., 'src/services/api.client' per TS
import convention), keeping the imported identifiers unchanged; ensure
TypeScript resolves the path via existing tsconfig paths if required.

In `@shapeai/apps/mobile/src/services/notification.service.ts`:
- Line 3: Change the relative import in notification.service.ts from
'./api.client' to the repo's absolute import form used elsewhere (e.g., import {
apiPost } from 'services/api.client' or the project's configured path alias), so
update the import statement for apiPost in the notification service to use the
project's absolute path convention and ensure it matches tsconfig/webpack path
aliases if applicable.

In `@shapeai/apps/mobile/tests/compare/compare.screen.test.tsx`:
- Around line 18-20: Replace the relative imports on the test file for
CompareScreen, apiGet/apiPost and useSubscription with their absolute module
paths; update the import statements that reference '../../app/(app)/compare',
'../../src/services/api.client', and '../../src/hooks/useSubscription' to use
the project's configured absolute paths (keeping the same imported symbols
CompareScreen, apiGet, apiPost, and useSubscription) so the test follows the
"use absolute imports" guideline.

In `@shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx`:
- Line 18: The tests currently mock/import modules using relative paths (e.g.,
'../../src/stores/auth.store', '../../src/stores/user.store',
'../../src/utils/notifications'); update these to use the project's TypeScript
absolute import convention (replace '../../src/stores/auth.store' with the
absolute path like 'src/stores/auth.store', do the same for the user store and
notifications imports) so the jest.mock and import statements in
notification.handler.test.tsx use absolute module paths consistent with the
TS/TSX config.

In `@shapeai/apps/mobile/tests/notifications/notification.service.test.ts`:
- Line 1: The test imports use relative paths; update them to absolute module
paths to follow the project convention — replace the relative import for
registerPushToken in notification.service.test.ts and any other relative
imports/mocks referenced around lines 12 and 17 with their corresponding
absolute module paths (e.g., use the package-root alias or tsconfig baseUrl
paths for the service and mock modules), ensuring imports reference the same
exported symbols (registerPushToken, and the mocks) but via absolute paths so
TypeScript resolves them consistently.

In `@shapeai/apps/mobile/tests/profile/profile.screen.test.tsx`:
- Around line 13-14: Replace the two relative imports at the top of the test
that pull in ProfileScreen and apiGet/apiPatch with project-root absolute
imports to follow the TS/TSX import convention; locate the import statements
referencing '../../app/(app)/profile' (ProfileScreen) and
'../../src/services/api.client' (apiGet, apiPatch) and update them to use the
repository's absolute module paths (e.g., the app module path for ProfileScreen
and the src/services api.client path) so the test uses absolute imports instead
of relative paths.

In `@shapeai/services/api-gateway/src/routes/profile.ts`:
- Around line 5-11: ProfileBody currently mixes POST and PATCH inputs; split it
into CreateProfileBody (used by the POST/create route) and PatchProfileBody
(used by the PATCH/update route). CreateProfileBody should include required
fields used on creation (height_cm, weight_kg, biological_sex, primary_goal) and
omit notifications_enabled if POST doesn't consume it; PatchProfileBody should
make fields optional and include notifications_enabled since updates may set it.
Update any route handlers or references in this file from ProfileBody to the
appropriate new type names (CreateProfileBody for create handler and
PatchProfileBody for patch handler) and adjust imports/exports accordingly.

In `@shapeai/services/api-gateway/src/routes/push-tokens.ts`:
- Around line 2-3: The imports in this file use relative paths; replace the two
relative imports ("import { pool } from '../db/client'" and "import {
requireAuth } from '../middleware/auth'") with the project's absolute import
style (use the configured path alias used across the repo) so they resolve via
the project's tsconfig/module-alias setup; update the import specifiers for pool
and requireAuth accordingly and verify the build/tsconfig path mapping supports
the chosen absolute base.

In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`:
- Line 7: Replace the relative import of the DB client with an absolute import:
change "import { pool } from '../../src/db/client'" to use the project's
absolute path (e.g. "import { pool } from 'src/db/client'") so the symbol pool
is imported via the repo's TS absolute import convention used across tests and
source code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0d5af887-c6f5-4b92-bf9d-7693f8628bfe

📥 Commits

Reviewing files that changed from the base of the PR and between 14bcaf1 and 825beda.

⛔ Files ignored due to path filters (1)
  • shapeai/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • shapeai/apps/mobile/app.json
  • shapeai/apps/mobile/app/(app)/_layout.tsx
  • shapeai/apps/mobile/app/(app)/compare.tsx
  • shapeai/apps/mobile/app/(app)/history.tsx
  • shapeai/apps/mobile/app/(app)/index.tsx
  • shapeai/apps/mobile/app/(app)/profile.tsx
  • shapeai/apps/mobile/app/_layout.tsx
  • shapeai/apps/mobile/jest.config.js
  • shapeai/apps/mobile/package.json
  • shapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsx
  • shapeai/apps/mobile/src/services/api.client.ts
  • shapeai/apps/mobile/src/services/notification.service.ts
  • shapeai/apps/mobile/tests/compare/compare.screen.test.tsx
  • shapeai/apps/mobile/tests/history/history.compare.test.tsx
  • shapeai/apps/mobile/tests/notifications/notification.handler.test.tsx
  • shapeai/apps/mobile/tests/notifications/notification.service.test.ts
  • shapeai/apps/mobile/tests/profile/profile.screen.test.tsx
  • shapeai/services/api-gateway/package.json
  • shapeai/services/api-gateway/src/db/migrations/004_notifications.sql
  • shapeai/services/api-gateway/src/routes/analyses.ts
  • shapeai/services/api-gateway/src/routes/profile.ts
  • shapeai/services/api-gateway/src/routes/push-tokens.ts
  • shapeai/services/api-gateway/src/server.ts
  • shapeai/services/api-gateway/src/services/notification.service.ts
  • shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts
  • shapeai/services/api-gateway/tests/notifications/notification.service.test.ts
  • shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts
✅ Files skipped from review due to trivial changes (4)
  • shapeai/services/api-gateway/src/db/migrations/004_notifications.sql
  • shapeai/apps/mobile/package.json
  • shapeai/apps/mobile/jest.config.js
  • shapeai/services/api-gateway/package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • shapeai/apps/mobile/app/(app)/history.tsx
  • shapeai/apps/mobile/src/components/history/AnalysisHistoryItem.tsx
  • shapeai/services/api-gateway/src/server.ts
  • shapeai/apps/mobile/app/(app)/_layout.tsx
  • shapeai/services/api-gateway/src/routes/analyses.ts

Comment on lines +14 to +23
useEffect(() => {
initialize()
}, [initialize])

useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener(() => {
router.push('/(app)/camera')
})
return () => sub.remove()
}, [])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Hook push-token registration into app startup/auth-ready flow.

This layout initializes auth and notification response handling, but never registers the device token. Without invoking registerPushToken(), backend token upsert won’t happen and push delivery for this device is likely broken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 14 - 23, The startup flow
calls initialize() but never invokes registerPushToken(), so device tokens are
never upserted; update the layout startup/auth-ready flow to call
registerPushToken() after initialize() (or once auth is confirmed) and
handle/await errors; specifically, inside the same useEffect that calls
initialize() (or a new effect that depends on initialize/auth-ready state) call
registerPushToken() and log or surface failures, ensuring you don’t duplicate
calls and that existing Notifications.addNotificationResponseReceivedListener
behavior is preserved.

Comment on lines +19 to +21
const sub = Notifications.addNotificationResponseReceivedListener(() => {
router.push('/(app)/camera')
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use notification payload route instead of hardcoding /(app)/camera.

Line 19 ignores the response object, so deep-link destinations from push data can’t be honored.

Proposed fix
-    const sub = Notifications.addNotificationResponseReceivedListener(() => {
-      router.push('/(app)/camera')
+    const sub = Notifications.addNotificationResponseReceivedListener((response) => {
+      const screen = response.notification.request.content.data?.screen
+      router.push(typeof screen === 'string' ? screen : '/(app)/camera')
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sub = Notifications.addNotificationResponseReceivedListener(() => {
router.push('/(app)/camera')
})
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const screen = response.notification.request.content.data?.screen
router.push(typeof screen === 'string' ? screen : '/(app)/camera')
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/_layout.tsx` around lines 19 - 21, The notification
handler registered with Notifications.addNotificationResponseReceivedListener
ignores the response and always routes to '/(app)/camera'; update the listener
callback to read the response parameter (the NotificationResponse) and extract
the deep-link route from response.notification.request.content.data (or the
relevant payload key), then call router.push with that route (falling back to
'/(app)/camera' if the payload route is missing). Ensure you reference the
existing listener setup (Notifications.addNotificationResponseReceivedListener)
and the router.push call when implementing the change.

Comment on lines +4 to +5
import { apiGet, apiPost } from '../../src/services/api.client'
import { useSubscription } from '../../src/hooks/useSubscription'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace relative imports with absolute imports.

Lines 4-5 use relative paths for app services/hooks.

As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 4 - 5, Update the two
relative imports in compare.tsx to use absolute paths: replace the import of
apiGet and apiPost from '../../src/services/api.client' with the absolute module
path that exports apiGet and apiPost (referenced symbol names: apiGet, apiPost)
and replace the import of useSubscription from '../../src/hooks/useSubscription'
with its absolute path (symbol: useSubscription); keep the imported symbol names
unchanged so existing usages in the file still resolve.

Comment on lines +36 to +50
const { id1, id2 } = useLocalSearchParams<{ id1: string; id2: string }>()
const { subscription } = useSubscription()
const isPro = subscription?.status === 'pro'
const compareRef = useRef<View>(null)

const [a1, setA1] = useState<AnalysisSummary | null>(null)
const [a2, setA2] = useState<AnalysisSummary | null>(null)
const [result, setResult] = useState<CompareResult | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isComparing, setIsComparing] = useState(false)

useEffect(() => {
Promise.all([
apiGet<AnalysisSummary>(`/analyses/${id1}`),
apiGet<AnalysisSummary>(`/analyses/${id2}`),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "compare.tsx" | head -5

Repository: SynkraAI/aiox-core

Length of output: 106


🏁 Script executed:

git ls-files | grep -E "compare\.tsx$"

Repository: SynkraAI/aiox-core

Length of output: 104


🏁 Script executed:

find . -type f -path "*mobile*compare.tsx"

Repository: SynkraAI/aiox-core

Length of output: 106


🏁 Script executed:

wc -l ./shapeai/apps/mobile/app/\(app\)/compare.tsx

Repository: SynkraAI/aiox-core

Length of output: 110


🏁 Script executed:

cat -n ./shapeai/apps/mobile/app/\(app\)/compare.tsx | head -100

Repository: SynkraAI/aiox-core

Length of output: 4258


🏁 Script executed:

rg -t ts -t tsx "useLocalSearchParams" --no-heading -A 1 -B 1 | head -50

Repository: SynkraAI/aiox-core

Length of output: 90


🏁 Script executed:

fd -type f -name "*.d.ts" | xargs grep -l "useLocalSearchParams" 2>/dev/null | head -5

Repository: SynkraAI/aiox-core

Length of output: 233


🏁 Script executed:

rg "useLocalSearchParams" -A 2 -B 1 | head -60

Repository: SynkraAI/aiox-core

Length of output: 4803


🏁 Script executed:

rg "useLocalSearchParams" --stats | head -20

Repository: SynkraAI/aiox-core

Length of output: 1555


🏁 Script executed:

find . -path "*/node_modules/expo-router*" -name "*.d.ts" 2>/dev/null | head -10

Repository: SynkraAI/aiox-core

Length of output: 44


🏁 Script executed:

rg "useLocalSearchParams" -B 5 -A 5 "shapeai/apps/mobile/app/(app)/analysis/\[id\].tsx" | head -40

Repository: SynkraAI/aiox-core

Length of output: 152


🌐 Web query:

expo-router useLocalSearchParams return type string array undefined

💡 Result:

The useLocalSearchParams hook from expo-router returns an object containing the current route's dynamic segments and query parameters as strings or string arrays (for catch-all routes [...slug]). Individual dynamic segments like [id] map to string values, while catch-all segments map to string[]. Optional query parameters can be string | undefined. Without generics, TypeScript infers { [key: string]: string | string[] | undefined }. With generics, you explicitly type it, e.g., useLocalSearchParams<{ user: string; everything: string[] }>. It can return undefined values for optional/missing parameters, or an empty object {} in certain contexts like unmatched routes or specific navigation scenarios (e.g., inside Stack.Screen options or layout files during navigation). Reserved names like params for catch-all routes cause bugs returning [object Object] instead of string[]. Examples from official docs: - /[user] -> { user: "evanbacon" } - /[user]?tab=projects -> { user: "bacon", tab: "projects" } or { user: "expo", tab: undefined } - /[...everything] -> { everything: ["evanbacon", "123"] } - /[...everything]?query=hello -> { everything: ["evanbacon", "123"], query: "hello" } Usage: const params = useLocalSearchParams<{ id: string; query?: string }>; // params.id: string (required route param) // params.query: string | undefined (optional query param)

Citations:


🏁 Script executed:

rg "compare" -A 2 -B 2 | grep -E "(push|replace|navigate|href)" | head -30

Repository: SynkraAI/aiox-core

Length of output: 542


🏁 Script executed:

rg "router\.push.*compare" -A 1 -B 1

Repository: SynkraAI/aiox-core

Length of output: 320


🏁 Script executed:

cat -n ./shapeai/apps/mobile/app/\(app\)/history.tsx | grep -A 20 -B 20 "selectedIds"

Repository: SynkraAI/aiox-core

Length of output: 5930


Add runtime validation for id1 and id2 before making API calls.

useLocalSearchParams with generics only provides compile-time type hints; TypeScript generics are erased at runtime. Query parameters can arrive as string | string[] | undefined. Validating these values before constructing API paths prevents invalid requests like /analyses/undefined or /analyses/value1,value2.

Consider adding a guard like:

if (!id1 || !id2 || Array.isArray(id1) || Array.isArray(id2)) {
  // Handle error or redirect
}

This applies to lines 49–50 (API calls) and 57 (POST payload).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 36 - 50, Validate
runtime types/values of id1 and id2 (from useLocalSearchParams) before using
them in API calls: inside the useEffect (and before any apiPost payload
construction), add a guard that rejects undefined or array values (e.g., !id1 ||
!id2 || Array.isArray(id1) || Array.isArray(id2)), handle the error by setting
isLoading/isComparing appropriately and returning early (or navigate/redirect),
and only call apiGet(`/analyses/${id1}`)/apiGet(`/analyses/${id2}`) and any
apiPost that uses id1/id2 after the guard passes so the requests never contain
"undefined" or comma-separated arrays.

Comment on lines +47 to +62
useEffect(() => {
Promise.all([
apiGet<AnalysisSummary>(`/analyses/${id1}`),
apiGet<AnalysisSummary>(`/analyses/${id2}`),
])
.then(([r1, r2]) => {
setA1(r1)
setA2(r2)
setIsLoading(false)
setIsComparing(true)
return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 })
})
.then(setResult)
.catch(() => setResult(null))
.finally(() => setIsComparing(false))
}, [id1, id2])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Clear isLoading on initial fetch failure to avoid infinite spinner.

If either apiGet fails, isLoading stays true, and the screen never leaves the loader state.

Proposed fix
-      .catch(() => setResult(null))
+      .catch(() => {
+        setResult(null)
+        setIsLoading(false)
+      })
       .finally(() => setIsComparing(false))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
Promise.all([
apiGet<AnalysisSummary>(`/analyses/${id1}`),
apiGet<AnalysisSummary>(`/analyses/${id2}`),
])
.then(([r1, r2]) => {
setA1(r1)
setA2(r2)
setIsLoading(false)
setIsComparing(true)
return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 })
})
.then(setResult)
.catch(() => setResult(null))
.finally(() => setIsComparing(false))
}, [id1, id2])
useEffect(() => {
Promise.all([
apiGet<AnalysisSummary>(`/analyses/${id1}`),
apiGet<AnalysisSummary>(`/analyses/${id2}`),
])
.then(([r1, r2]) => {
setA1(r1)
setA2(r2)
setIsLoading(false)
setIsComparing(true)
return apiPost<CompareResult>('/analyses/compare', { analysis_id_1: id1, analysis_id_2: id2 })
})
.then(setResult)
.catch(() => {
setResult(null)
setIsLoading(false)
})
.finally(() => setIsComparing(false))
}, [id1, id2])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/app/`(app)/compare.tsx around lines 47 - 62, The
useEffect chain currently sets setIsLoading(false) only on successful apiGet
calls so a failure leaves isLoading true; update the Promise.all(...) call to
ensure setIsLoading(false) runs on both success and failure (e.g., add a
.catch(err => { setResult(null); throw err }) before continuing or move
setIsLoading(false) into a .finally() directly after Promise.all), and ensure
setIsComparing is cleared in all paths (use .finally(() =>
setIsComparing(false)) for the compare request and also call
setIsComparing(false) if Promise.all fails) so that setIsLoading and
setIsComparing are always reset when apiGet or apiPost fail; refer to useEffect,
apiGet, apiPost('/analyses/compare'), setA1, setA2, setResult, setIsLoading, and
setIsComparing to locate the changes.

Comment on lines +49 to +61
const res = await axios.post(EXPO_PUSH_URL, {
to: user.token,
title: 'ShapeAI',
body,
data: { screen: '/(app)/camera' },
})
const result = res.data?.data
if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') {
await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token])
}
} catch {
// log per-user failures silently — don't abort the whole batch
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -a "notification.service.ts" --type f

Repository: SynkraAI/aiox-core

Length of output: 223


🏁 Script executed:

cat -n /home/jailuser/git/shapeai/services/api-gateway/src/services/notification.service.ts | head -100

Repository: SynkraAI/aiox-core

Length of output: 2445


🏁 Script executed:

cat -n /home/jailuser/git/shapeai/services/api-gateway/src/db/client.ts

Repository: SynkraAI/aiox-core

Length of output: 404


🏁 Script executed:

rg -t ts "console\.(log|warn|error|info)" shapeai/services/api-gateway/src/ --max-count=10

Repository: SynkraAI/aiox-core

Length of output: 668


🏁 Script executed:

cat -n /home/jailuser/git/shapeai/services/api-gateway/src/server.ts | head -80

Repository: SynkraAI/aiox-core

Length of output: 2249


Add timeout and error logging to push notification handler.

The axios.post call (line 49) lacks a timeout, and the catch block (lines 59–61) silently swallows errors without any logging. This hides individual push delivery failures, making it difficult to detect and debug reliability issues. While the outer error handler in server.ts will catch total job failures, per-user errors are completely invisible.

Proposed fix
-      const res = await axios.post(EXPO_PUSH_URL, {
+      const res = await axios.post(EXPO_PUSH_URL, {
         to: user.token,
         title: 'ShapeAI',
         body,
         data: { screen: '/(app)/camera' },
-      })
+      }, { timeout: 10000 })
-    } catch {
-      // log per-user failures silently — don't abort the whole batch
+    } catch (err) {
+      console.warn('push_send_failed', { userId: user.id, token: user.token, err })
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await axios.post(EXPO_PUSH_URL, {
to: user.token,
title: 'ShapeAI',
body,
data: { screen: '/(app)/camera' },
})
const result = res.data?.data
if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') {
await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token])
}
} catch {
// log per-user failures silently — don't abort the whole batch
}
const res = await axios.post(EXPO_PUSH_URL, {
to: user.token,
title: 'ShapeAI',
body,
data: { screen: '/(app)/camera' },
}, { timeout: 10000 })
const result = res.data?.data
if (result?.status === 'error' && result?.details?.error === 'DeviceNotRegistered') {
await pool.query('DELETE FROM push_tokens WHERE token = $1', [user.token])
}
} catch (err) {
console.warn('push_send_failed', { userId: user.id, token: user.token, err })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/src/services/notification.service.ts` around
lines 49 - 61, The per-user push send currently calls axios.post(EXPO_PUSH_URL,
...) without a timeout and swallows all errors; update the axios.post in
notification.service.ts to include a reasonable timeout option (e.g., 3–10s) and
change the catch to capture the error object and log a concise contextual
message including the user.token and error details (error.message and, if
present, error.response.data) while still not throwing so the batch continues;
keep the existing DeviceNotRegistered check and pool.query deletion logic
intact.

Comment on lines +3 to +11
vi.mock('../../src/db/client', () => ({
pool: { query: vi.fn() },
}))

vi.mock('axios', () => ({
default: { post: vi.fn() },
}))

import { pool } from '../../src/db/client'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use absolute module paths in test mocks/imports.

Line 3 and Line 11 use relative paths (../../src/...). Please switch these to the project’s absolute import pattern for consistency with repo rules.

As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts` around
lines 3 - 11, The test uses a relative module path in the vi.mock call and the
import of pool (the string argument to vi.mock(...) and the import { pool }
statement) — update both to use the project's absolute-import pattern (replace
the relative '../../src/db/client' module specifiers with the repo's configured
absolute module path for the same module), keeping the mock shape (vi.mock(...,
{ pool: { query: vi.fn() } })) and import name unchanged so tests still
reference the same pool symbol.

Comment on lines +20 to +41
async function simulateCompare(userId: string, id1: string, id2: string, authUserId = userId) {
const { rows } = await mockPool.query(
`SELECT id, scores FROM analyses WHERE id = ANY($1) AND user_id = $2 AND status = 'completed'`,
[[id1, id2], authUserId]
)

if (rows.length !== 2) {
throw Object.assign(new Error('Forbidden'), { status: 403 })
}

const a1 = rows.find((r: { id: string }) => r.id === id1)
const a2 = rows.find((r: { id: string }) => r.id === id2)

const claudeRes = await mockAxios.post('https://api.anthropic.com/v1/messages', {
model: 'claude-sonnet-4-6',
messages: [{ role: 'user', content: `${JSON.stringify(a1.scores)} ${JSON.stringify(a2.scores)}` }]
}, {})

const raw: string = claudeRes.data.content[0].text
const clean = raw.replace(/```json|```/g, '').trim()
return JSON.parse(clean)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -path "*analyses*" | head -20

Repository: SynkraAI/aiox-core

Length of output: 328


🏁 Script executed:

git ls-files | grep -E "(analyses|compare)" | grep -E "\.(ts|js)$"

Repository: SynkraAI/aiox-core

Length of output: 320


🏁 Script executed:

fd -e ts -e js "analyses" | head -30

Repository: SynkraAI/aiox-core

Length of output: 320


🏁 Script executed:

wc -l shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts

Repository: SynkraAI/aiox-core

Length of output: 134


🏁 Script executed:

wc -l shapeai/services/api-gateway/src/routes/analyses.ts

Repository: SynkraAI/aiox-core

Length of output: 118


🏁 Script executed:

cat -n shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts

Repository: SynkraAI/aiox-core

Length of output: 3572


🏁 Script executed:

cat -n shapeai/services/api-gateway/src/routes/analyses.ts

Repository: SynkraAI/aiox-core

Length of output: 11374


🏁 Script executed:

rg "fastify.*inject" shapeai/services/api-gateway/tests/ -A 2

Repository: SynkraAI/aiox-core

Length of output: 44


🏁 Script executed:

rg "\.inject\(" shapeai/services/api-gateway/tests/ -B 2 -A 2

Repository: SynkraAI/aiox-core

Length of output: 44


🏁 Script executed:

head -50 shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts

Repository: SynkraAI/aiox-core

Length of output: 1856


🏁 Script executed:

fd "setup\|fixture\|vitest.config" shapeai/services/api-gateway/tests/ -o shapeai/services/api-gateway/

Repository: SynkraAI/aiox-core

Length of output: 240


🏁 Script executed:

ls -la shapeai/services/api-gateway/tests/

Repository: SynkraAI/aiox-core

Length of output: 411


🏁 Script executed:

cat shapeai/services/api-gateway/tests/analyses/analyses.routes.test.ts

Repository: SynkraAI/aiox-core

Length of output: 4273


🏁 Script executed:

find shapeai/services/api-gateway -name "*.test.ts" -exec head -30 {} +

Repository: SynkraAI/aiox-core

Length of output: 7845


Test the real route handler instead of duplicating compare logic in simulateCompare.

This helper mirrors backend behavior with critical differences: the route includes a detailed system prompt and structured message for Claude, passes required API headers (x-api-key, anthropic-version), sets max_tokens: 1024, and returns status codes via reply.status(403). Tests can pass while the actual route fails or behaves differently due to missing headers, incomplete prompts, or divergent error handling. Use fastify.inject against the registered POST /analyses/compare route to exercise real behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/analyses/analyses.compare.test.ts` around
lines 20 - 41, The helper simulateCompare duplicates route logic and omits
required request details; replace its usage in tests by calling the real POST
/analyses/compare route via fastify.inject so the actual handler executes.
Construct the injected request to include the same payload and headers the route
expects: set method POST, url '/analyses/compare', JSON body with model, system
and user messages (mirror the route's structured system prompt and messages),
include max_tokens: 1024, and set required headers like 'x-api-key' and
'anthropic-version'; use mockPool to prepare DB rows as before and assert
response status (e.g. reply.status === 403) and parsed JSON instead of parsing
mockAxios.raw content. Ensure you remove or stop using simulateCompare (and its
mockAxios call) and reference the actual route's handler behavior in your
assertions.

Comment on lines +3 to +14
vi.mock('../../src/db/client', () => ({
pool: { query: vi.fn() },
}))

vi.mock('axios', () => ({
default: { post: vi.fn() },
}))

import { pool } from '../../src/db/client'
import axios from 'axios'
import { getEligibleUsers, pickTemplate, sendReanalysisNotifications } from '../../src/services/notification.service'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use absolute internal module paths in this test.

The internal module references in mocks/imports are relative (../../src/...).

As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@shapeai/services/api-gateway/tests/notifications/notification.service.test.ts`
around lines 3 - 14, The test currently imports/mocks internal modules using
relative paths ('../../src/db/client' and
'../../src/services/notification.service'); change these to absolute internal
module paths (e.g. 'src/db/client' and 'src/services/notification.service') in
both the vi.mock(...) and import lines so mocks and imports reference the same
absolute modules (retain the axios mock as-is), ensuring symbols like pool,
getEligibleUsers, pickTemplate, and sendReanalysisNotifications are imported
from the absolute paths.

Comment on lines +11 to +20
async function simulatePostPushToken(userId: string, token: string, platform: string) {
if (!token || !platform) throw Object.assign(new Error('Bad Request'), { status: 400 })

await mockPool.query(
`INSERT INTO push_tokens (user_id, token, platform) VALUES ($1, $2, $3)
ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, updated_at = NOW()`,
[userId, token, platform]
)
return { ok: true }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This suite is not testing POST /push-tokens; it’s testing a local replica.

simulatePostPushToken re-implements route logic, so regressions in the actual Fastify route (auth, schema, handler wiring) won’t be caught. Replace helper-based assertions with server-level route tests (e.g., inject against the real app).

Also applies to: 25-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/services/api-gateway/tests/push-tokens/push-tokens.routes.test.ts`
around lines 11 - 20, The tests are bypassing the real route by calling the
helper simulatePostPushToken which re-implements route logic; replace usages of
simulatePostPushToken and direct mockPool.query assertions with server-level
requests against the actual POST /push-tokens endpoint (e.g., use app.inject or
fastify.inject against your real app instance), supplying authentication headers
and JSON body to exercise route auth, schema, and handler wiring, then assert
response status/body and verify DB effects (mockPool or test DB) afterwards;
remove or deprecate the simulatePostPushToken helper to ensure future
regressions in the POST /push-tokens route are caught.

Ryan and others added 2 commits May 1, 2026 17:10
Add missing test 4.4: technical purchase failure shows generic Alert.
All 95 mobile tests + 28 api-gateway tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx (1)

68-81: ⚡ Quick win

Successful-purchase test doesn't verify pollUntilPro was called.

The test correctly asserts that navigation occurs, but the subscription-polling step (pollUntilPro) is a critical part of the happy path — it ensures the user's pro status is confirmed before redirecting. Without asserting it was awaited, a refactor that drops the poll and navigates immediately would still pass this test.

♻️ Suggested addition
  mockPurchase.mockResolvedValueOnce({})
+ // Capture the pollUntilPro spy so we can assert it was called
+ const pollUntilProMock = jest.fn().mockResolvedValue(undefined)
  mockUseSubscription.mockReturnValue({
    subscription: { status: 'free', expires_at: null },
    isLoading: false,
-   pollUntilPro: jest.fn().mockResolvedValue(undefined),
+   pollUntilPro: pollUntilProMock,
  })

  const { getByTestId } = render(<PaywallScreen />)
  await act(flushAllPromises)

  fireEvent.press(getByTestId('btn-assinar-pro'))

  await waitFor(() => {
    expect(mockReplace).toHaveBeenCalledWith('/(app)')
  })

+ expect(pollUntilProMock).toHaveBeenCalledTimes(1)
  expect(mockPurchase).toHaveBeenCalledTimes(1)

(The mockReturnValue override can live inside the test or replace the one in beforeEach for this case.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 68
- 81, The test currently asserts navigation and purchase but omits verifying the
subscription poll; update the 'navega para home após compra bem-sucedida' test
in PaywallScreen (paywall.purchase.test.tsx) to also assert that the polling
helper (pollUntilPro / its test double) was called and awaited: ensure the mock
for pollUntilPro (or the exported helper used by PaywallScreen) is set to
resolve (e.g., mockResolvedValueOnce) before firing the purchase, then add an
expectation such as expect(mockPollUntilPro).toHaveBeenCalled() or
toHaveBeenCalledTimes(1) (and/or check call order with mockReplace if needed) so
the test fails if the component no longer waits for pollUntilPro before
navigating.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Line 12: The tests use relative module keys in jest.mock (e.g., the mock for
'../../src/services/purchases.service' and '../../src/hooks/useSubscription')
which can fail to intercept the real modules if the app uses absolute/path-alias
imports; change these mocks to use the same resolved module keys as the source
(e.g., the absolute/path-alias form used by paywall.tsx such as
'@/services/purchases.service' and '@/hooks/useSubscription'), update all five
mock locations accordingly, and verify the alias matches your tsconfig.json
paths / jest.config.js moduleNameMapper so Jest registers the mock under the
exact same module identity the component imports.
- Around line 63-65: The current test uses waitFor(() =>
expect(mockReplace).not.toHaveBeenCalled()) which is a false-positive because
waitFor only retries on thrown errors; change the test to first wait for a
positive observable signal that the async error path has completed (e.g. waitFor
the purchase button to become enabled/not disabled or for a loading indicator to
be removed using the same waitFor utility), then after that waitFor completes,
assert that mockReplace (the mocked router.replace) was not called; reference
the existing symbols mockReplace, purchasePackage (the rejected promise),
router.replace and the button/loading element to locate where to add the
positive waitFor and move the not.toHaveBeenCalled() assertion outside of
waitFor.

---

Nitpick comments:
In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx`:
- Around line 68-81: The test currently asserts navigation and purchase but
omits verifying the subscription poll; update the 'navega para home após compra
bem-sucedida' test in PaywallScreen (paywall.purchase.test.tsx) to also assert
that the polling helper (pollUntilPro / its test double) was called and awaited:
ensure the mock for pollUntilPro (or the exported helper used by PaywallScreen)
is set to resolve (e.g., mockResolvedValueOnce) before firing the purchase, then
add an expectation such as expect(mockPollUntilPro).toHaveBeenCalled() or
toHaveBeenCalledTimes(1) (and/or check call order with mockReplace if needed) so
the test fails if the component no longer waits for pollUntilPro before
navigating.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 99fea957-a114-408f-b7cb-0529f5c52f83

📥 Commits

Reviewing files that changed from the base of the PR and between 825beda and 9713abd.

📒 Files selected for processing (2)
  • shapeai/apps/mobile/app/(app)/index.tsx
  • shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx
✅ Files skipped from review due to trivial changes (1)
  • shapeai/apps/mobile/app/(app)/index.tsx

router: { replace: jest.fn(), push: jest.fn() },
}))

jest.mock('../../src/services/purchases.service', () => ({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Relative imports violate project guidelines and risk breaking jest.mock interception.

All five locations use ../../… relative paths instead of absolute imports. Beyond the guideline violation, there's a functional risk: returning a falsy condition is not sufficient to trigger a retry — the callback must throw an error in order to retry is not the concern here, but the core issue is module identity. If paywall.tsx follows the project convention and imports via absolute paths or path aliases (e.g. @/services/purchases.service), then jest.mock('../../src/services/purchases.service', …) and jest.mock('../../src/hooks/useSubscription', …) may register against a different resolved key in Jest's module registry, meaning the mocks silently fail to intercept the source file's actual imports — and every test in this suite could be exercising the real implementations.

♻️ Proposed fix
-jest.mock('../../src/services/purchases.service', () => ({
+jest.mock('@/services/purchases.service', () => ({
   getOfferings: jest.fn().mockResolvedValue({ ... }),
   purchasePackage: jest.fn(),
   restorePurchases: jest.fn(),
   PURCHASES_ERROR_CODE: { PURCHASE_CANCELLED_ERROR: 'purchaseCancelled' },
 }))

-jest.mock('../../src/hooks/useSubscription', () => ({
+jest.mock('@/hooks/useSubscription', () => ({
   useSubscription: jest.fn(),
 }))

-import PaywallScreen from '../../app/(app)/paywall'
+import PaywallScreen from '@/app/(app)/paywall'
 import { router } from 'expo-router'
-import { purchasePackage, restorePurchases, PURCHASES_ERROR_CODE } from '../../src/services/purchases.service'
-import { useSubscription } from '../../src/hooks/useSubscription'
+import { purchasePackage, restorePurchases, PURCHASES_ERROR_CODE } from '@/services/purchases.service'
+import { useSubscription } from '@/hooks/useSubscription'

(Alias prefix may differ — align with the paths config in tsconfig.json / jest.config.js moduleNameMapper.)

As per coding guidelines, "Use absolute imports instead of relative imports in all code."

Also applies to: 24-24, 28-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` at line 12, The
tests use relative module keys in jest.mock (e.g., the mock for
'../../src/services/purchases.service' and '../../src/hooks/useSubscription')
which can fail to intercept the real modules if the app uses absolute/path-alias
imports; change these mocks to use the same resolved module keys as the source
(e.g., the absolute/path-alias form used by paywall.tsx such as
'@/services/purchases.service' and '@/hooks/useSubscription'), update all five
mock locations accordingly, and verify the alias matches your tsconfig.json
paths / jest.config.js moduleNameMapper so Jest registers the mock under the
exact same module identity the component imports.

Comment on lines +63 to +65
await waitFor(() => {
expect(mockReplace).not.toHaveBeenCalled()
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

waitFor(() => expect(not.toHaveBeenCalled())) is a false-positive trap.

waitFor retries only when the callback throws; returning a passing assertion never triggers a retry. Because expect(mockReplace).not.toHaveBeenCalled() passes immediately (on the very first poll, before the rejected promise from purchasePackage has even settled), this test will always be green — even if the component incorrectly calls router.replace after a cancelled purchase.

The fix is to first wait for a positive, observable signal that the async error path has completed (e.g. the button becoming pressable again, or a loading indicator disappearing), then assert no navigation occurred outside waitFor.

♻️ Suggested approach
  fireEvent.press(getByTestId('btn-assinar-pro'))

- await waitFor(() => {
-   expect(mockReplace).not.toHaveBeenCalled()
- })
+ // Wait for the UI to settle after the rejected purchase (e.g. button re-enabled)
+ await waitFor(() => {
+   expect(getByTestId('btn-assinar-pro')).not.toBeDisabled()
+ })
+ expect(mockReplace).not.toHaveBeenCalled()

(Replace the not.toBeDisabled() sentinel with whatever observable state the component exposes after error handling completes.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await waitFor(() => {
expect(mockReplace).not.toHaveBeenCalled()
})
fireEvent.press(getByTestId('btn-assinar-pro'))
// Wait for the UI to settle after the rejected purchase (e.g. button re-enabled)
await waitFor(() => {
expect(getByTestId('btn-assinar-pro')).not.toBeDisabled()
})
expect(mockReplace).not.toHaveBeenCalled()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shapeai/apps/mobile/tests/paywall/paywall.purchase.test.tsx` around lines 63
- 65, The current test uses waitFor(() =>
expect(mockReplace).not.toHaveBeenCalled()) which is a false-positive because
waitFor only retries on thrown errors; change the test to first wait for a
positive observable signal that the async error path has completed (e.g. waitFor
the purchase button to become enabled/not disabled or for a loading indicator to
be removed using the same waitFor utility), then after that waitFor completes,
assert that mockReplace (the mocked router.replace) was not called; reference
the existing symbols mockReplace, purchasePackage (the rejected promise),
router.replace and the button/loading element to locate where to add the
positive waitFor and move the not.toHaveBeenCalled() assertion outside of
waitFor.

Ryan and others added 20 commits May 2, 2026 02:10
… for Python 3.14

- mediapipe 0.10.14→0.10.35 (Python 3.14 compat), migrate to Tasks API
- psycopg2-binary, pillow, numpy: pin relaxed to >= for forward compat
- Add .gitignore to api-gateway and ai-engine services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass size:true option and wrap in try/catch for forward compat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…env var

- Buffer.from(secret, 'base64') to correctly verify Supabase JWTs
- Add AWS_S3_BUCKET to .env (s3.service.ts uses AWS_S3_BUCKET, not S3_BUCKET_NAME)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fastify rejects requests with Content-Type: application/json but empty body.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…end-to-end

- Migration 005: ADD COLUMN body_composition JSONB to reports table (applied)
- GET /analyses/:id: include r.body_composition in SELECT and response
- AnalysisResult: add BodyComposition interface and optional field
- report.tsx: BodyCompositionCard showing fat%, body type, fat areas, coach assessment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tasks API calibrates confidence differently than Solutions API.
Old API used min_detection_confidence=0.5 (Solutions); equivalent
sensitivity in Tasks API requires ~0.3 to match prior behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If pose landmarks not detected, pipeline continues with neutral_scores()
(all geometric metrics = 50, body_fat from BMI). Claude Vision still
runs and generates report/plan from visual analysis. Analysis never
fails due to pose detection alone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace single-attempt detection with 6-step fallback sequence:
original → resize → contrast+1.3 → contrast+1.6 → resize+contrast → min confidence.
Each step lowers confidence threshold and increases preprocessing aggressiveness.
Also adds ImageOps.exif_transpose to fix phone camera rotation metadata.
Tested: dark (0.3x), overexposed (2.5x), low quality (30% JPEG), portrait resize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostgreSQL numeric columns return decimal.Decimal which breaks float
arithmetic in score_calculator and vision_analyzer. Convert at source
in analysis.py router and defensively in each consumer function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…3, 5.4)

- POST /chat endpoint with Claude AI integration and context injection
- 3 coach personas: Rafael (técnico), Marina (motivacional), Bruno (intenso)
- Rate limiting: 20 msgs/day Free, unlimited Pro (chat_usage table)
- GET /chat/usage for UI counter
- coach.tsx chat screen with quick suggestions and paywall on limit
- Persona selector in profile screen
- coach_persona column in user_profiles (migration 006)
- chat_usage table for daily rate limiting (migration 006)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useFocusEffect replaces useEffect so switching persona in profile
is immediately reflected in the coach screen header name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each message now includes the full session history so Claude maintains
context across turns. History capped at 20 entries server-side to
keep token usage bounded. Stateless between sessions (v1 spec).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- InlineText component parses **bold** patterns into fontWeight:bold spans
- Chat context now includes full Week 1 exercise list (name, sets, reps,
  rest) so coach can explain, substitute and adjust the real plan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove MediaPipe from pipeline entirely — no more geometric landmarks
- Vision prompt now returns 9 muscle group scores (quadriceps, glutes,
  calves, biceps, triceps, chest, abs, traps, lats) each with a note,
  plus overall_score, strengths_summary, weaknesses_summary
- report_generator simplified: builds highlights/dev_areas from muscle
  scores directly — eliminates a redundant Claude API call
- plan_generator updated to use new muscle score priority list
- Chat context updated to use new body_composition fields
- New report UI: body composition → strengths/weaknesses summary →
  muscle breakdown with score bar + note per group
- BodyScores types updated across mobile + shared package

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth: replace @fastify/jwt with jose JWKS for ES256 (Supabase)
- s3: fix download to use GetObject instead of presigned PUT url
- db: fix query to JOIN user_profiles (biological_sex, primary_goal)
- ai-engine: add load_dotenv() to main.py
- api-gateway: normalize INTERNAL_SECRET with ?? ''
- api-client: send Content-Type only when body is present
- plan_generator: max_tokens=8192, strip markdown fences, fallback plan
- report_generator: strip markdown fences, fallback report
- vision_analyzer: replace MediaPipe with Claude Vision muscle scoring
- workout.tsx: make getUserProfile optional in Promise.all
- onboarding: add onboarding screen flow
- analysis: add _layout.tsx for nested routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
eas.json estava sem seção env — variáveis do .env não eram injetadas
no bundle, causando crash na inicialização do Supabase client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…th style

- BodyScores interface (shared + mobile) estava sem o campo shoulders
  causando divisão incorreta por 10 em calculateOverallScore
- heroWrapper tinha width duplicado (100% e 120%) — TS1117

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rafaelscosta
Copy link
Copy Markdown
Collaborator

Fechando como parte da triagem de manutenção do backlog de PRs abertos do AIOX-Core em 2026-05-07.

Motivo: este PR ficou defasado ou está fora do escopo atual do core, com conflito/estado antigo ou conteúdo de produto/app/superfície legada que não deve entrar diretamente em main.

Se ainda houver uma ideia útil aqui, o caminho correto é abrir um PR novo, pequeno e atualizado contra main, com story/critério de aceite e validação atual. Obrigado pela contribuição.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent system related area: cli CLI tools (bin/, packages/aios-pro-cli/) area: core Core framework (.aios-core/core/) area: devops CI/CD, GitHub Actions (.github/) area: docs Documentation (docs/) area: health-check Health check system area: installer Installer and setup (packages/installer/) area: pro Pro features (pro/) area: synapse SYNAPSE context engine area: workflows Workflow system related mcp squad type: test Test coverage and quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants