Final project jfvg Entrega final — Lexio MVP (flujo E2E + documentación) - #280
Final project jfvg Entrega final — Lexio MVP (flujo E2E + documentación)#280fredyvizcarra wants to merge 3 commits into
Conversation
📝 WalkthroughWalkthroughLexio’s MVP is introduced as a React Native/Expo app with Firebase authentication and Firestore persistence, an Express BFF integrating Claude and Unsplash, daily vocabulary practice with streak tracking, bilingual UI, backend tests, Firebase configuration, and extensive product and delivery documentation. ChangesLexio MVP
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (28)
DEPLOYMENT.md-47-57 (1)
47-57: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winKeep the documented Firebase storage bucket consistent with
mobile/app.json.Line 54 uses an
.appspot.combucket, butmobile/app.jsonuseslexio-dev-8e8e3.firebasestorage.app. Copying this guide can configure a different or invalid bucket. Document the exact project value or instruct users to copy it from the generated Firebase configuration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DEPLOYMENT.md` around lines 47 - 57, Update the Firebase storage bucket value in the Mobile app.json configuration example to match the exact value used by mobile/app.json, lexio-dev-8e8e3.firebasestorage.app, or instruct readers to copy the generated Firebase configuration value instead of using a hardcoded bucket..gitignore-11-14 (1)
11-14: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winIgnore service-account and production environment files.
DEPLOYMENT.mdinstructs developers to createservice-account.json(Lines 39-41), but this file is not ignored..env.productionis also unprotected. Add explicit patterns before credentials are accidentally committed.Proposed fix
# Environment variables .env .env.local .env.*.local +.env.production +.env.development # Firebase .firebase/ firebase-debug.log +service-account.jsonAlso applies to: 24-26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.gitignore around lines 11 - 14, Update the Environment variables section of .gitignore to explicitly ignore service-account.json and .env.production, placing both patterns with the existing environment and credential exclusions.1-descripcion-general-del-producto.md-275-288 (1)
275-288: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse a LAN-reachable backend URL for physical Expo Go devices.
localhostresolves to the phone, so the documented QR flow cannot reach the backend. Documenthttp://<development-machine-LAN-IP>:3000(or a tunnel) for devices; reservelocalhostfor simulators.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@1-descripcion-general-del-producto.md` around lines 275 - 288, Actualiza la variable EXPO_PUBLIC_API_URL en la documentación del flujo con Expo Go para usar una URL accesible por LAN, como http://<development-machine-LAN-IP>:3000, o indicar un túnel; conserva localhost únicamente para simuladores.backend/src/repositories/SessionRepository.ts-74-97 (1)
74-97: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMake session completion atomic.
Two concurrent completion requests can both read
completed: false, overwrite answers, and each continue to streak processing. Use a Firestore transaction that checks ownership andcompletedbefore updating; return a conflict to the losing request.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/repositories/SessionRepository.ts` around lines 74 - 97, Update the session completion method in SessionRepository to perform the read, ownership check, completed check, and session update inside a Firestore transaction. Reject sessions already marked completed with a conflict result, and ensure only the transaction that successfully updates the session proceeds to streak processing; preserve the existing answer scoring and completion fields.backend/src/routes/sessions.ts-13-22 (1)
13-22: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRequire one answer for every session exercise.
Array length alone accepts duplicate or unknown
exerciseIdvalues. Validate unique IDs here, then haveSessionServiceverify the submitted ID set exactly matches the session’s exercises before allowing completion.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/routes/sessions.ts` around lines 13 - 22, Update completeSessionSchema to reject duplicate exerciseId values, then update SessionService’s completion flow to verify the submitted unique ID set exactly matches the session’s exercise IDs, rejecting missing or unknown exercises before completing the session.6-tickets-de-trabajo.md-162-182 (1)
162-182: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not grant clients direct access to server-managed sessions and streaks.
These owner-only rules still let an authenticated user read
correctAnswervalues and directly create or update their own session completion, score, and streak. Since the BFF owns this logic, deny direct client access todailySessionsandstreaks; Firebase Admin operations remain allowed despite restrictive rules.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@6-tickets-de-trabajo.md` around lines 162 - 182, Update the Firestore rules for the dailySessions and streaks match blocks to deny all direct client reads and writes, removing the authenticated-owner allow rules. Leave WordCards permissions unchanged; server-side Firebase Admin operations must continue to function.backend/src/repositories/SessionRepository.ts-21-33 (1)
21-33: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winEnforce one daily session across completed and concurrent requests. Completed sessions are ignored by the lookup, so
createDailySessioncreates a replacement; simultaneous starts can also both create a session.
backend/src/repositories/SessionRepository.ts#L21-L33: query and return the session regardless of completion state.backend/src/repositories/SessionRepository.ts#L44-L66: create through a transaction or deterministic(userId, sessionDate)document key, then return 409 for completed sessions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/repositories/SessionRepository.ts` around lines 21 - 33, Update findInProgressByDate to return the matching daily session regardless of its completed state. In createDailySession, enforce uniqueness for each userId/sessionDate using a transaction or deterministic document key, and return a 409 conflict when the existing session is completed.6-tickets-de-trabajo.md-217-219 (1)
217-219: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winEnforce normalized-term uniqueness atomically. A read-then-create application check allows simultaneous requests for the same normalized term to both succeed.
6-tickets-de-trabajo.md#L217-L219: require an atomic uniqueness strategy in the ticket contract.backend/src/repositories/WordRepository.ts#L86-L101: use a transaction or deterministic collision-safe document ID derived from(userId, normalizedTerm).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@6-tickets-de-trabajo.md` around lines 217 - 219, Replace the ticket contract’s application-only normalized-term uniqueness requirement with an atomic strategy. In backend/src/repositories/WordRepository.ts around the word creation flow, use a transaction or a deterministic collision-safe document ID derived from userId and normalizedTerm so concurrent requests cannot create duplicates; update the repository behavior and documentation consistently.6-tickets-de-trabajo.md-712-712 (1)
712-712: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftExclude incomplete drafts from practice eligibility. Persisted cards without a selected image are counted toward the four-word threshold and can enter
image_matchexercises with an empty image URL.
6-tickets-de-trabajo.md#L712-L712: define an explicit draft state or defer persistence until the image is selected.backend/src/repositories/WordRepository.ts#L67-L84: exclude drafts from both the vocabulary count and session word selection queries.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@6-tickets-de-trabajo.md` at line 712, Define an explicit draft state for cards without a selected image, or defer persistence until image selection is complete, in the workflow described at 6-tickets-de-trabajo.md:712. Update backend/src/repositories/WordRepository.ts:67-84 so vocabulary-count and session-word-selection queries exclude drafts, preventing incomplete cards from meeting thresholds or entering image_match exercises.firestore.rules-5-25 (1)
5-25: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRestrict direct client writes to BFF-owned Firestore documents.
The BFF design places mutations behind Express and Firebase Admin, but these rules permit authenticated clients to write protected state directly. Admin SDK writes bypass Firestore Rules, so deny client writes to server-managed collections or strictly validate immutable ownership and every permitted field.
firestore.rules#L5-L25: deny direct client writes towordCards,dailySessions, andstreaks(or narrowly validate allowed fields); preventuserIdchanges on updates.2-arquitectura-del-sistema.md#L340-L350: replace the owner-write example and describe the same server-owned write policy.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@firestore.rules` around lines 5 - 25, Restrict direct client mutations to the BFF-owned collections in firestore.rules: deny client create, update, delete, and write access for wordCards, dailySessions, and streaks, while preserving only the required authenticated read access and preventing any userId changes if client updates remain supported. Update 2-arquitectura-del-sistema.md lines 340-350 to replace the owner-write example with the same server-owned write policy; both sites must consistently state that mutations go through Express/Firebase Admin.backend/src/tests/SessionService.test.ts-96-106 (1)
96-106: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winDo not create a second daily session after completion.
createDailySession()only checksfindInProgressByDate, so completing today’s session and then calling creation should still return the completed session rather than callingcreate. Add guard/repository behavior that rejects or returns after a completed today session, and adjust this test to expect that blocked behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/tests/SessionService.test.ts` around lines 96 - 106, The createDailySession flow in SessionService must prevent duplicate sessions after today’s session is completed. Extend the repository lookup or add a completed-session guard so an existing completed session is returned or creation is rejected without calling mockSessionRepo.create, then update the test case to assert that blocked behavior.backend/src/services/SessionService.ts-120-139 (1)
120-139: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAlways generate the promised ten exercises.
With four to nine cards,
sampleWordsreturns fewer than ten cards, so this creates fewer than ten exercises; with exactly four, it creates no MCQs. Build a ten-item selection with intentional repetition when the vocabulary is smaller thanSESSION_SIZE.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/SessionService.ts` around lines 120 - 139, Update the session generation flow around sampleWords, imageMatchWords, and mcqWords to always produce SESSION_SIZE (ten) selected cards, repeating cards intentionally when allWords contains fewer than SESSION_SIZE entries. Preserve the existing half split and exercise-building behavior, including MCQs for small vocabularies, while ensuring the returned exercises array always has ten items.backend/src/integrations/claudeClient.ts-65-74 (1)
65-74: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValidate the parsed MCQ payload at runtime.
JSON.parse(...) as ...does not ensure four unique options, a non-empty question, or thatcorrectAnsweris one of the options. Reject/retry invalid model output before persisting a broken session.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/integrations/claudeClient.ts` around lines 65 - 74, Update the Claude MCQ parsing flow around the JSON.parse result to perform runtime validation before returning the payload. Ensure question is non-empty, options contains exactly four unique non-empty values, and correctAnswer is one of those options; reject invalid output through the existing parse-error/retry path before persistence.backend/src/services/SessionService.ts-162-171 (1)
162-171: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftComplete the session and update the streak atomically.
If
SessionRepository.completesucceeds butStreakService.updateAfterSessionfails, the request returns500; retries then hitSESSION_ALREADY_COMPLETED, leaving the streak permanently stale. Commit both documents in one Firestore transaction.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/SessionService.ts` around lines 162 - 171, Update the session-completion flow around SessionRepository.complete and StreakService.updateAfterSession to execute both document updates within a single Firestore transaction. Ensure either both the session completion and streak update commit together or neither does, while preserving the existing Session not found handling.backend/src/services/WordService.ts-19-41 (1)
19-41: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftEnforce logical uniqueness atomically. Firestore composite indexes support queries; they do not reject duplicate field values. Separate read-then-write operations allow concurrent duplicate cards and same-day sessions.
backend/src/services/WordService.ts#L19-L41: perform duplicate detection and creation in one transaction or use a deterministic unique document key.backend/src/services/SessionService.ts#L115-L139: transactionally create at most one session per user/date.3-modelo-de-datos.md#L107-L116: document the transactional/deterministic-key mechanism instead of presenting indexes as uniqueness constraints.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/WordService.ts` around lines 19 - 41, Enforce logical uniqueness atomically: in backend/src/services/WordService.ts lines 19-41, update the WordService creation flow to use a transaction or deterministic unique document key so duplicate detection and WordRepository.create cannot race; in backend/src/services/SessionService.ts lines 115-139, transactionally ensure at most one session exists per user/date; in 3-modelo-de-datos.md lines 107-116, document the chosen transaction or deterministic-key mechanism and stop describing composite indexes as uniqueness constraints.backend/src/controllers/SessionController.ts-7-8 (1)
7-8: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winSeparate persisted exercises from the public session DTO. The response currently exposes
correctAnswer, allowing clients to submit perfect answers without solving the exercises; it also wraps a response that OpenAPI declares as a rootDailySession.
backend/src/controllers/SessionController.ts#L7-L8: map persisted exercises to a client-safe DTO that omitscorrectAnswer, then serialize the documented top-level response shape.4-especificaciones-de-la-api.md#L205-L245: define a public exercise schema withoutcorrectAnswerrather than requiring an undeclared internal field.4-especificaciones-de-la-api.md#L247-L289: align the DailySession response schema with the chosen controller envelope.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/controllers/SessionController.ts` around lines 7 - 8, Update SessionController to map persisted exercises into a client-safe DTO that omits correctAnswer, then return the documented DailySession response envelope instead of exposing the raw session or an inconsistent wrapper. In 4-especificaciones-de-la-api.md lines 205-245, define the public exercise schema without correctAnswer; in lines 247-289, align the DailySession schema with the controller’s chosen top-level response shape.backend/src/services/SessionService.ts-115-118 (1)
115-118: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReconcile existing-session handling with the API contract.
The service returns an in-progress session, which the controller sends as
201, while the OpenAPI contract specifies409for an existing in-progress session. Make this explicitly idempotent (200) or reject it as documented.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/SessionService.ts` around lines 115 - 118, Update the existing-session branch in the SessionService flow around SessionRepository.findInProgressByDate so its behavior matches the API contract: either explicitly preserve the session as an idempotent success that the controller returns with 200, or reject the existing in-progress session through the documented 409 path. Do not leave it returning normally in a way that produces 201.backend/src/services/WordService.ts-27-41 (1)
27-41: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not discard caller-provided definition and image fields.
The documented create payload accepts
definition,imageUrl, andunsplashPhotoId, but this path always generates a definition and persists an empty image URL. Honor supplied values (and only call Claude when no definition was provided), or remove these fields from the API contract.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/WordService.ts` around lines 27 - 41, Update the word creation flow around generateDefinition, searchImages, and WordRepository.create to preserve caller-provided definition, imageUrl, and unsplashPhotoId values. Invoke generateDefinition only when body.definition is absent, use generated results only as fallbacks for missing image fields, and persist the resolved values instead of always writing an empty image URL or null photo ID.backend/src/integrations/unsplashClient.ts-17-23 (1)
17-23: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a timeout to the Unsplash image request.
WordService.createWordawaitssearchImagesinsidePromise.all, so a hanging Unsplash connection can keep the word-create request open indefinitely. Abort thefetchwithAbortSignal.timeout(...)or an equivalent bounded deadline and wrap the integration failure incatch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/integrations/unsplashClient.ts` around lines 17 - 23, Update the Unsplash request in searchImages to use a bounded timeout by supplying an AbortSignal.timeout or equivalent deadline to fetch. Add catch handling around the integration request so timeout and other fetch failures are wrapped and propagated with useful Unsplash-specific context.backend/scripts/seedFirestore.ts-14-15 (1)
14-15: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winDo not source-control or log a usable demo password.
This script can target any configured Firebase project, including production. Require externally supplied demo credentials, refuse non-development environments, and never print the password.
Also applies to: 79-81
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/scripts/seedFirestore.ts` around lines 14 - 15, Update the seed script’s demo credential handling around email and password so credentials are supplied externally rather than hardcoded or logged. Require an externally provided password, refuse to run unless the configured Firebase environment is explicitly development, and ensure all output excludes the password; apply the same protections to the related credential usage at the referenced location.Source: Linters/SAST tools
mobile/app/(tabs)/home.tsx-62-74 (1)
62-74: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd the required visible keyboard focus state.
The practice button has no
focus:ring-2 focus:ring-indigo-500state or React Native equivalent. Add a focus-awarePressable/NativeWind implementation with a visible Indigo 500 ring.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/home.tsx around lines 62 - 74, Add keyboard-focus handling to the practice button around TouchableOpacity/handleStartPractice, replacing or adapting it to a focus-aware Pressable implementation that visibly renders an Indigo 500, 2px-equivalent focus ring while focused. Preserve the existing styles, loading disabled behavior, press handler, and button text/activity indicator.Source: Coding guidelines
mobile/services/api.ts-15-15 (1)
15-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winSet a finite HTTP timeout on the mobile API client.
axios.create()omitstimeout, so requests through this client can remain open until the platform closes them; add a configured timeout before awaiting these calls in mobile UI flows.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/services/api.ts` at line 15, Update the axios client created by the `axios.create` call to include a finite timeout configuration alongside `baseURL`, using the project’s established timeout constant or value if available.mobile/package.json-11-31 (1)
11-31: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAlign Expo Router and React DOM with the SDK 56 dependency set.
mobile/package.jsondeclares Expo SDK 56, butexpo-router@~5.0.7belongs to the previous Router line andreact-dom@^19.2.7does not matchreact@19.2.3. Re-resolve the Expo 56 dependency set insidemobile/and use the recommended SDK 56 Router version so install/build is not blocked by dependency conflicts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/package.json` around lines 11 - 31, Update the dependency declarations in mobile/package.json to use the Expo SDK 56-compatible expo-router version and align react-dom with the declared react 19.2.3 version. Re-resolve and commit the mobile dependency lockfile so the dependency graph matches the SDK 56 recommendations and installs without conflicts.mobile/app/(tabs)/add-word.tsx-56-75 (1)
56-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winStore actions that throw on failure are called without try/catch in several screens.
sessionStore.ts'sstartSession/completeSessionestablish a convention where store actions reject on failure and callers must catch them (asadd-word.tsx'shandleGeneratecorrectly does); the sites below don't follow it, so failures are silently swallowed with no user feedback.
mobile/app/(tabs)/add-word.tsx#L56-L75: wrapawait updateWord(...)in try/catch and surface an error viasetError.mobile/app/card/[id].tsx#L40-L43: wrapawait updateWord(id, { definition })in try/catch before showing the success alert.mobile/app/card/[id].tsx#L45-L49: wrapawait updateWord(id, { status: newStatus })in try/catch before callingrouter.back().mobile/app/(tabs)/settings.tsx#L21-L26: wrapawait logout()in try/catch so the store resets/navigation still happen or an error is shown on failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/add-word.tsx around lines 56 - 75, Store actions can reject without caller handling, so add try/catch around each affected await and surface failures to the user. In mobile/app/(tabs)/add-word.tsx lines 56-75, update handleSave to catch updateWord errors and call setError; in mobile/app/card/[id].tsx lines 40-43, catch the definition update before showing the success alert; in mobile/app/card/[id].tsx lines 45-49, catch the status update before router.back(); and in mobile/app/(tabs)/settings.tsx lines 21-26, catch logout failures while preserving the intended reset/navigation or showing an error.mobile/app.json-36-36 (1)
36-36: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse a device-reachable BFF URL.
localhostresolves to the phone when running Expo Go, so the mobile client cannot reach the backend. Use a LAN URL for local-device testing and an HTTPS deployment URL for release builds. (docs.expo.dev)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app.json` at line 36, Update the EXPO_PUBLIC_API_URL configuration in app.json to use a device-reachable LAN backend URL for local Expo Go testing instead of localhost, and use the HTTPS deployment URL for release builds through the project’s existing environment-specific configuration.mobile/app/(auth)/login.tsx-66-76 (1)
66-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAdd visible keyboard focus states to every manual button.
None of these controls implements the required
focus:ring-2 focus:ring-indigo-500treatment, leaving keyboard users without a visible focus indicator.
mobile/app/(auth)/login.tsx#L66-L76: implement the required focus ring on the login control.mobile/app/(auth)/register.tsx#L70-L80: implement the required focus ring on the registration control.mobile/app/results.tsx#L46-L58: implement the required focus ring on both result-navigation controls.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(auth)/login.tsx around lines 66 - 76, Apply the required visible keyboard focus ring styling to every manual button: the login control in mobile/app/(auth)/login.tsx lines 66-76, the registration control in mobile/app/(auth)/register.tsx lines 70-80, and both result-navigation controls in mobile/app/results.tsx lines 46-58. Update each button’s styling to include the equivalent of focus:ring-2 and focus:ring-indigo-500 while preserving existing behavior and styles.Source: Coding guidelines
mobile/app.json-43-45 (1)
43-45: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winReplace the placeholder EAS project ID.
In
mobile/app.json,extra.eas.projectIdis stillyour-eas-project-id, which breaks EAS build/update project identification. Runeas initfrommobileand commit the generated project ID.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app.json` around lines 43 - 45, Replace the placeholder value in the extra.eas.projectId configuration with the actual project ID generated by running eas init from the mobile directory, then commit that generated ID without changing the surrounding EAS configuration.Source: Learnings
mobile/services/firebase.ts-30-37 (1)
30-37: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInitialize with AsyncStorage before calling
getAuth.
getAuth(appInstance)returns the existing initialized Auth instance and does not apply the React Native persistence provided in the fallback. Initialize first, and only fall back togetAuthif that throws.Proposed fix
function createAuth(appInstance: FirebaseApp): Auth { try { - return getAuth(appInstance); - } catch { return initializeAuth(appInstance, { persistence: getReactNativePersistence(AsyncStorage), }); + } catch { + return getAuth(appInstance); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/services/firebase.ts` around lines 30 - 37, Update createAuth so it first calls initializeAuth with getReactNativePersistence(AsyncStorage) and returns that initialized instance; if initialization throws because Auth is already configured, catch the error and fall back to getAuth(appInstance).
🟡 Minor comments (19)
MUESTRA_DEL_PRODUCTO.md-24-24 (1)
24-24: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd explicit languages to all fenced Markdown blocks.
Both documentation files contain fenced blocks without a language identifier, triggering MD040.
MUESTRA_DEL_PRODUCTO.md#L24-L24: use```textfor the ASCII flow diagram.TESTING.md#L25-L25: use```textfor the expected Jest output.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@MUESTRA_DEL_PRODUCTO.md` at line 24, Update the fenced block at MUESTRA_DEL_PRODUCTO.md lines 24-24 to specify the text language for the ASCII flow diagram, and update the fenced block at TESTING.md lines 25-25 similarly for the expected Jest output; make no other documentation changes.Source: Linters/SAST tools
MUESTRA_DEL_PRODUCTO.md-17-17 (1)
17-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix minor Spanish copy errors.
Use “generados por IA” on Line 17, “Escoger imagen” on Line 147, and “Resultado de práctica” on Line 217.
Also applies to: 147-147, 217-217
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@MUESTRA_DEL_PRODUCTO.md` at line 17, Corrige los errores menores de redacción en MUESTRA_DEL_PRODUCTO.md: cambia “generada por IA” por “generados por IA” en la línea 17, “Escoge imagen” por “Escoger imagen” en la línea 147 y “Resultados de práctica” por “Resultado de práctica” en la línea 217.DEPLOYMENT.md-12-13 (1)
12-13: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReplace the repository URL placeholder.
git clone [url-repo]is not executable documentation. Use the actual repository URL so a fresh developer can follow the setup without editing the command first.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@DEPLOYMENT.md` around lines 12 - 13, Replace the [url-repo] placeholder in the DEPLOYMENT.md cloning command with the repository’s actual URL, while preserving the surrounding setup commands.7-pull-requests.md-26-35 (1)
26-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix the Markdown formatting warnings.
Add
textortypescript/jsonlanguage identifiers to the untyped fenced blocks, and surround the table with blank lines so markdownlint passes cleanly.Also applies to: 65-88, 91-113, 116-124, 152-159, 171-209
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@7-pull-requests.md` around lines 26 - 35, Update the fenced code blocks in 7-pull-requests.md, including the ranges referenced by the review, with appropriate language identifiers such as text, typescript, or json. Add blank lines immediately before and after the table so markdownlint formatting checks pass without changing the documented content.Source: Linters/SAST tools
backend/.env.example-4-6 (1)
4-6: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not advertise unsupported service-account paths.
firebaseAdmin.tsonly parsesFIREBASE_SERVICE_ACCOUNTas JSON, so a JSON-file path fails at startup. Either implement file loading or remove the path option.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/.env.example` around lines 4 - 6, Update the FIREBASE_SERVICE_ACCOUNT documentation to describe only the supported single-line service-account JSON value, removing the JSON-file path option unless firebaseAdmin.ts is also updated to load files.backend/scripts/seedFirestore.ts-45-63 (1)
45-63: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winMake word-card seeding idempotent.
Every run creates six new random documents for the same user, duplicating cards in subsequent demos. Use deterministic IDs or upsert existing cards by
userIdandnormalizedTerm.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/scripts/seedFirestore.ts` around lines 45 - 63, Update the word-card seeding loop in seedFirestore.ts to use deterministic document IDs or query existing records by userId and normalizedTerm before writing. Ensure repeated runs upsert or reuse the same cards rather than creating new random documents, while preserving cardIds collection and existing field values.backend/package.json-13-13 (1)
13-13: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDeclare ESLint in the backend dependencies.
npm run lintuseseslint 'src/**/*.ts', butbackend/package.jsonhas no localeslintdependency and there are no repository ESLint config files. Add ESLint plus compatible dependencies and config so the script is reproducible outside CI/environmental fallbacks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/package.json` at line 13, Add ESLint as a local backend dependency and add the compatible parser/plugin dependencies and repository ESLint configuration required by the existing lint script. Ensure npm install can resolve the declared versions and npm run lint works reproducibly without relying on globally installed tools or CI-provided configuration.backend/src/routes/words.ts-9-12 (1)
9-12: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject whitespace-only terms before passing them to downstream APIs.
z.string().min(1)accepts" ", andWordService.createWordthen callsgenerateDefinition(body.term.trim(), ...)andsearchImages(body.term.trim(), ...)with an empty string. Move the trimmed required validation into the schema, e.g.z.string().trim().min(1, ...), so empty/whitespace-only submission fails at validation instead of at the AI/image integration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/routes/words.ts` around lines 9 - 12, Update createWordSchema’s term validator to trim input before applying the required minimum-length check, while preserving the existing maximum length and error message. This must reject whitespace-only terms during schema validation before WordService.createWord reaches downstream APIs.mobile/app/(tabs)/home.tsx-141-149 (1)
141-149: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winFix disabled-state contrast.
#9CA3AFon#F3F4F6does not meet the required contrast threshold. Use a darker label and a visible border or higher-contrast disabled surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/home.tsx around lines 141 - 149, Update the disabled practice button styles, practiceButtonDisabled and practiceButtonTextDisabled, to meet the required contrast threshold by using a darker text color and adding a visible border or higher-contrast background surface.Source: Coding guidelines
mobile/.gitignore-33-34 (1)
33-34: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winIgnore all real
.envfiles.This misses
.envand non-local variants, so copied configuration can be committed. Expo recommends excluding.envfiles from version control. (docs.expo.dev)Proposed fix
- .env*.local + .env + .env.* + !.env.example🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/.gitignore` around lines 33 - 34, Update the environment-file patterns in the gitignore entries to exclude the base `.env` file and all `.env` variants, including non-local files, while retaining coverage for existing local environment files.mobile/app/(tabs)/home.tsx-96-149 (1)
96-149: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winApply Inter typography to the text styles.
home.tsx’s text styles don’t declare afontFamily, so React Native will use the platform default font. Add the loaded Inter family/weight names to the text styles used in this screen.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/home.tsx around lines 96 - 149, Add the loaded Inter font family and corresponding weight names to the text styles in home.tsx, including logo, streakText, stats, practiceButtonText, practiceHint, and practiceButtonTextDisabled. Preserve each style’s existing sizing, color, alignment, and spacing while matching each font weight to the available Inter variant.Source: Coding guidelines
mobile/app/(tabs)/settings.tsx-21-26 (1)
21-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
handleLogoutdoesn't handlelogout()failures.If
logout()throws, the subsequent store resets androuter.replacenever run, and no error is shown — the user is left on the Settings screen with no feedback.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/settings.tsx around lines 21 - 26, Update handleLogout to catch failures from logout(), preserve the store reset and router.replace flow when logout succeeds, and surface a user-visible error when it fails so the user receives feedback instead of remaining silently on Settings.mobile/app/(tabs)/add-word.tsx-66-74 (1)
66-74: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSuccess confirmation alerts use an empty title and a bare checkmark glyph. Both save flows show
Alert.alert('', '✓', ...), which conveys no meaningful information to sighted or screen-reader users.
mobile/app/(tabs)/add-word.tsx#L66-L74: use a localized title/message, e.g.Alert.alert(t('addWord.savedTitle'), t('addWord.savedMessage'), ...).mobile/app/card/[id].tsx#L42-L42: same fix, e.g.Alert.alert(t('cardDetail.savedTitle'), t('cardDetail.savedMessage'), ...).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/add-word.tsx around lines 66 - 74, The success confirmation alerts use an empty title and opaque checkmark message. Update the Alert.alert calls in mobile/app/(tabs)/add-word.tsx lines 66-74 and mobile/app/card/[id].tsx line 42 to use localized savedTitle and savedMessage translation keys, preserving their existing actions and navigation behavior.mobile/app/card/[id].tsx-35-35 (1)
35-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHardcoded, non-localized fallback text.
"Card not found" bypasses
useTranslation, unlike every other string in this file, breaking the bilingual UI for this reachable edge case (stale/deep-linkedid).🌐 Suggested fix
- <Text style={{ textAlign: 'center', marginTop: 40 }}>Card not found</Text> + <Text style={{ textAlign: 'center', marginTop: 40 }}>{t('cardDetail.notFound')}</Text>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/card/`[id].tsx at line 35, Replace the hardcoded “Card not found” text in the card detail fallback with the existing useTranslation localization flow. Add or reuse an appropriate translation key and render its translated value while preserving the current fallback styling and behavior.mobile/app/practice.tsx-122-140 (1)
122-140: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse a stable index-based key for MCQ options.
exercise.optionscomes from LLM-generated content, not an option id, sokey={option}can duplicate when the generated text contains the same option more than once. Pass the array index through themapcallback or attach/remove a hidden stable id; changing it here is safe, but don’t rely onoptionas the key.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/practice.tsx` around lines 122 - 140, Update the exercise.options map callback in the practice screen to receive the array index and use that stable index-based value for each TouchableOpacity key. Keep the option text and selection behavior unchanged, and stop using option as the key.mobile/app/(tabs)/_layout.tsx-11-17 (1)
11-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winIncrease inactive-tab contrast.
#9CA3AFon white is below the required contrast ratio for tab labels and UI states. Use a darker neutral such as#6B7280.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/_layout.tsx around lines 11 - 17, Update the tabBarInactiveTintColor setting in the tab bar configuration to a darker neutral, such as `#6B7280`, while leaving the active tint and tab bar styling unchanged.Source: Coding guidelines
mobile/app/results.tsx-20-23 (1)
20-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winValidate result query parameters before rendering.
A deep link such as
?correct=abcrendersNaN; duplicate query keys can also arrive as arrays. Parse finite non-negative integers and clampcorrecttototalbefore calculating the result.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/results.tsx` around lines 20 - 23, Update the result parameter parsing around correctNum, totalNum, and streakNum to safely handle scalar or array query values, accepting only finite non-negative integers and applying the existing defaults otherwise. Ensure total is valid before calculating isPerfect, and clamp correct to total so malformed or excessive values cannot produce invalid result state.mobile/app/(auth)/register.tsx-30-31 (1)
30-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMove remaining user-facing literals into i18n.
These strings remain English after changing locale, contradicting the bilingual UI scope.
mobile/app/(auth)/register.tsx#L30-L31: replace the password-length literal with a translation key.mobile/app/(tabs)/_layout.tsx#L20-L25: localize the Home tab title.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(auth)/register.tsx around lines 30 - 31, Move the password-length message in register.tsx into the existing i18n resources and use its translation key in the validation error. Also localize the Home tab title in _layout.tsx through the same translation mechanism; update both affected files as specified.mobile/app/(tabs)/_layout.tsx-24-24 (1)
24-24: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winHide decorative tab icons from screen readers.
Each emoji duplicates the tab label and is read aloud by screen readers. Add
aria-hidden="true"to the tab iconTextcomponents at lines 24, 31, 38, and 45 so only the tab label is announced.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/_layout.tsx at line 24, Add aria-hidden="true" to the Text components returned by each tabBarIcon in the tab layout, including the icons at lines 24, 31, 38, and 45, so decorative emojis are hidden from screen readers while tab labels remain announced.Source: Coding guidelines
🧹 Nitpick comments (5)
2-arquitectura-del-sistema.md (1)
317-323: 📐 Maintainability & Code Quality | 🔵 TrivialDocument the OTA update flow.
Add EAS Update channels/branches plus rollout and rollback steps; otherwise JavaScript-only fixes require a new binary build. Based on learnings: Use Expo EAS Build and Updates for continuous deployment and over-the-air updates.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@2-arquitectura-del-sistema.md` around lines 317 - 323, Amplía la sección “Mobile (Expo)” para documentar el flujo de actualizaciones OTA con EAS Update: define los canales o ramas utilizados, explica cómo publicar una actualización y cómo promoverla durante el rollout, e incluye los pasos para revertir a una versión estable. Mantén diferenciados los cambios JavaScript que pueden distribuirse OTA de los cambios nativos que requieren un nuevo build.Source: Learnings
backend/src/repositories/StreakRepository.ts (1)
16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffUse camelCase for exported repository singleton objects. Both are const objects, not classes. As per coding guidelines, “Use camelCase for variable and function names.”
backend/src/repositories/StreakRepository.ts#L16-L16: renameStreakRepositorytostreakRepositoryand update imports.backend/src/repositories/UserRepository.ts#L6-L6: renameUserRepositorytouserRepositoryand update imports.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/repositories/StreakRepository.ts` at line 16, Rename the exported singleton object StreakRepository to streakRepository in backend/src/repositories/StreakRepository.ts:16-16 and update every import and reference. Rename UserRepository to userRepository in backend/src/repositories/UserRepository.ts:6-6 and update every import and reference, preserving behavior.Source: Coding guidelines
mobile/LICENSE (1)
1-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winLeftover Expo template license misattributes copyright.
This is Expo's own MIT license text (copyright "650 Industries, Inc. (aka Expo)"), copied verbatim from the
create-expo-appscaffold. As committed, it incorrectly states that this project's code is copyrighted by Expo rather than the actual project owners.📄 Suggested fix
-Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) +Copyright (c) 2026 <project author/organization>Or remove this file entirely if the project doesn't intend to publish under Expo's license.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/LICENSE` around lines 1 - 22, Remove the leftover Expo template LICENSE file, or replace its Expo-specific copyright attribution with the actual project owners and the intended project license text.mobile/app/(tabs)/add-word.tsx (2)
99-110: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winInteractive elements without visible text lack accessibility labels. The coding guidelines require
aria-label-equivalent descriptive labels on interactive elements lacking visible text; in React Native this maps toaccessibilityLabel/accessibilityRole, which are absent here.
mobile/app/(tabs)/add-word.tsx#L99-L110: addaccessibilityRole="button"andaccessibilityLabel={t('addWord.selectImage')}(or similar) to the image-onlyTouchableOpacity.mobile/app/(tabs)/dictionary.tsx#L29-L48: addaccessibilityRole="button"andaccessibilityLabel(e.g.`View ${item.term}`) to the cardTouchableOpacityto expose it as a single labeled action.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/add-word.tsx around lines 99 - 110, The image-only TouchableOpacity in mobile/app/(tabs)/add-word.tsx lines 99-110 must expose accessibilityRole="button" and a translated descriptive accessibilityLabel such as t('addWord.selectImage'). Also update the card TouchableOpacity in mobile/app/(tabs)/dictionary.tsx lines 29-48 with accessibilityRole="button" and a label identifying the item, such as “View ${item.term}”.Source: Coding guidelines
183-260: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo screen sets the required 'Inter' font family. The coding guidelines mandate
'Inter'(with system fallbacks) for all typography, but every reviewedStyleSheet.create()block across the mobile app omitsfontFamily, so text renders in the platform default font everywhere.
mobile/app/(tabs)/add-word.tsx#L183-L260: addfontFamily: 'Inter'(or a shared text style) totitle,label,input,buttonText, etc.mobile/App.tsx#L13-L20: addfontFamilyto theTextstyle if this component remains in use.mobile/app/(tabs)/dictionary.tsx#L89-L132: addfontFamilytotitle,tabText,cardTerm,cardDefinition, etc.mobile/app/(tabs)/settings.tsx#L82-L122: addfontFamilytotitle,sectionLabel,langText,logoutText, etc.mobile/app/card/[id].tsx#L110-L158: addfontFamilytoterm,label,saveButtonText, etc.mobile/app/practice.tsx#L157-L206: addfontFamilytoprogress,questionText,optionText, etc.Consider centralizing this in a shared theme/typography module instead of repeating it in every
StyleSheet.create().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mobile/app/`(tabs)/add-word.tsx around lines 183 - 260, Apply the required Inter font family consistently across all typography styles, preferably through a shared theme or typography module. Update styles in mobile/app/(tabs)/add-word.tsx lines 183-260, mobile/App.tsx lines 13-20, mobile/app/(tabs)/dictionary.tsx lines 89-132, mobile/app/(tabs)/settings.tsx lines 82-122, mobile/app/card/[id].tsx lines 110-158, and mobile/app/practice.tsx lines 157-206, including each title, label, text, input, button, and content style used for rendered text; preserve existing visual properties and use the shared style where appropriate.Source: Coding guidelines
Entrega final del proyecto académico Lexio. Incluye un MVP funcional de extremo
a extremo (auth, captura de palabras, práctica diaria, racha), backend/mobile,
tests unitarios del backend y documentación de despliegue y muestra del producto.
No representa una versión productiva cerrada; debido a que es una app mobile, use expo go y no quise pagar la cuenta dev de ios, tests E2E automatizados en mobile y funcionalidades post-MVP.
Summary by CodeRabbit
New Features
Documentation
Tests