- Cliente Supabase singleton en src/lib/supabase.ts - Interfaces AuthUser / AuthService en src/auth/types.ts - SupabaseAuthService: signInWithGoogle, signOut, getCurrentUser, onAuthStateChange con upsert a tabla users (platform_admin para @inquality.com.py, guest resto) - AuthProvider (React Context) wrappea toda la app; expone user, loading, signIn, signOut - LoginPage /login: logo InQuality color, título, fade-in, botón Google naranja #F59845 - RootComponent del router guarda todas las rutas — sin sesión → /login, con sesión + /login → / - Script SQL: supabase/migrations/001_create_users.sql (tabla users + RLS) - AppHeader: <Link> → <a href="/"> para evitar dependencia de RouterContext en tests - 561/561 tests verdes, build sin errores TS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
476 lines
19 KiB
Markdown
476 lines
19 KiB
Markdown
# Sprint 4 — BRIEF
|
|
|
|
> **Naturaleza del sprint:** refactorización arquitectural mayor.
|
|
> No hay features de producto nuevas visibles. El objetivo es habilitar colaboración multi-usuario sobre una base correcta.
|
|
> Este sprint es una excepción justificada al rebalanceo web/PDF/infra — es 100% infraestructura.
|
|
|
|
---
|
|
|
|
## Contexto y motivación
|
|
|
|
InQ ROI nació como herramienta single-user con persistencia local (IndexedDB/Dexie). El equipo de InQuality lo usa actualmente de forma individual y quiere pasar a un modelo colaborativo donde:
|
|
|
|
- Múltiples consultores pueden trabajar sobre procesos compartidos
|
|
- Existe identidad de usuario (quién creó, quién actualizó)
|
|
- Algunos elementos son privados del consultor, otros compartidos con el equipo
|
|
- Los recursos son un catálogo global del equipo, no duplicados por proceso
|
|
|
|
Para esto se requiere reemplazar IndexedDB con una base de datos centralizada, agregar autenticación, y rediseñar el modelo de datos de recursos.
|
|
|
|
**Dato importante:** no hay datos en producción que valgan la pena migrar. Se abandona IndexedDB limpiamente y se empieza de cero en la nueva base de datos.
|
|
|
|
---
|
|
|
|
## Decisiones estratégicas — NO reabrir en esta sprint
|
|
|
|
### Auth: Supabase ahora, Keycloak después
|
|
|
|
Se usa **Supabase** como BaaS para Sprint 4: auth + PostgreSQL + RLS. La razón es velocidad de implementación.
|
|
|
|
El código debe construir una **interfaz `AuthService`** que abstraiga el proveedor de autenticación. El objetivo es que la migración futura a Keycloak (servidor de identidad InQuality para múltiples productos) sea cambio de implementación, no reescritura de la app.
|
|
|
|
InQuality tiene infraestructura VPS (desa + prod) y la visión es Keycloak self-hosted como plataforma de identidad para todos sus productos. Eso es trabajo de un sprint futuro.
|
|
|
|
### Modelo de plataforma: InQuality como operador (Modelo A)
|
|
|
|
InQ ROI es una **plataforma de consultoría operada por InQuality**, no un SaaS peer-to-peer. InQuality controla quién entra, a qué tiene acceso, y qué puede hacer. Los clientes y consultores externos son guests invitados por InQuality.
|
|
|
|
Esto se implementa con un campo `platform_role` en la tabla de usuarios:
|
|
|
|
- `platform_admin` → staff InQuality, puede ver todos los procesos de todos los usuarios/grupos
|
|
- `member` → usuario estándar (consultor InQuality sin acceso global, o reservado para futuro)
|
|
- `guest` → cliente externo o consultor externo, solo ve lo que se le comparte explícitamente
|
|
|
|
En Sprint 4, todos los usuarios de InQuality reciben `platform_admin`. El rol `guest` se usa cuando se agregue el primer cliente externo.
|
|
|
|
**Migración a Modelo B (SaaS peer-to-peer) — posible, no planificada.**
|
|
Cuando InQuality decida que cada organización cliente debe ser autónoma (administrar sus propios usuarios, sin acceso implícito de InQuality), la migración consiste en: eliminar el bypass de `platform_admin`, agregar self-service de org, y cambiar las políticas RLS. El schema de Sprint 4 ya lo soporta sin reestructurar datos.
|
|
|
|
### Auth: providers múltiples según rol
|
|
|
|
- **Staff InQuality** (`platform_admin`): Google Workspace `@inquality.com.py`. Solo este dominio puede obtener `platform_admin`.
|
|
- **Guests** (clientes, consultores externos): cualquier email. Magic link o email+contraseña. No requieren Google Workspace.
|
|
|
|
Supabase soporta múltiples providers simultáneamente. En Sprint 4 solo se implementa Google OAuth para staff. La infraestructura para guest auth (magic link) se deja preparada pero no activada hasta que haya un cliente real.
|
|
|
|
### Online-only
|
|
|
|
Se elimina el requerimiento de offline. La herramienta requiere conectividad. Offline + sync se agrega al roadmap futuro.
|
|
|
|
### Sin auditoría en Sprint 4
|
|
|
|
La auditoría de cambios (quién cambió qué campo y cuándo) se difiere a un sprint dedicado. En Sprint 4 solo se captura `owner_id`, `created_by`, `updated_by`, `created_at`, `updated_at` en las tablas principales — suficiente para mostrar identidad en la UI sin el costo de instrumentar un audit log completo.
|
|
|
|
---
|
|
|
|
## Modelo de datos objetivo
|
|
|
|
### Entidades centrales y sus relaciones
|
|
|
|
El modelo tiene tres entidades de identidad/acceso y las entidades de negocio del producto. La separación es importante: `client` en un proceso es un atributo de texto, **no** una entidad con acceso al sistema.
|
|
|
|
```
|
|
Usuarios (identidad)
|
|
↓ pertenecen a
|
|
Grupos de usuario (agrupación de acceso)
|
|
↓ se usan en
|
|
Acceso a procesos (tabla de permisos)
|
|
↓ determina quién ve
|
|
Procesos → Actividades → Simulaciones (entidades de negocio)
|
|
↑
|
|
Recursos (catálogo global, sin process_id)
|
|
```
|
|
|
|
### Usuarios
|
|
|
|
La tabla de usuarios combina Supabase Auth (fuente de verdad para credenciales) con una tabla `users` en la DB de la app para atributos de la plataforma.
|
|
|
|
```
|
|
users
|
|
id uuid PK (mismo UUID que auth.users de Supabase)
|
|
email text NOT NULL
|
|
name text
|
|
avatar_url text
|
|
platform_role 'platform_admin' | 'member' | 'guest'
|
|
created_at timestamptz DEFAULT now()
|
|
```
|
|
|
|
**Reglas de `platform_role`:**
|
|
- `platform_admin` → staff InQuality. Bypass de todos los controles de acceso: ve y puede editar todos los procesos. Solo usuarios con dominio `@inquality.com.py` obtienen este rol.
|
|
- `member` → usuario estándar. Ve solo lo que se le comparte explícitamente. Reservado para consultores InQuality sin acceso global o para uso futuro.
|
|
- `guest` → cliente externo o consultor externo. Idéntico a `member` en permisos. Se usa para distinguir visualmente en la UI quién es externo.
|
|
|
|
En Sprint 4, todos los usuarios de InQuality reciben `platform_admin`. El rol `guest` entra cuando llegue el primer cliente externo.
|
|
|
|
**El campo `platform_role` nunca hardcodea "inquality".** Si en el futuro InQuality decide operar el Modelo B (cada org autónoma), el bypass se elimina cambiando las políticas RLS — el campo permanece genérico.
|
|
|
|
### Grupos de usuario
|
|
|
|
Permiten compartir un proceso con un conjunto de personas sin listar a cada una individualmente. Útil para "todos los del cliente Acme" o "equipo de consultoría junior".
|
|
|
|
```
|
|
user_groups
|
|
id uuid PK
|
|
name text NOT NULL
|
|
created_by uuid → users.id
|
|
created_at timestamptz DEFAULT now()
|
|
|
|
group_memberships
|
|
user_id uuid → users.id
|
|
group_id uuid → user_groups.id
|
|
role 'owner' | 'member'
|
|
PRIMARY KEY (user_id, group_id)
|
|
```
|
|
|
|
### Acceso a procesos
|
|
|
|
Reemplaza el campo `visibility: 'private' | 'team'` del modelo anterior. El acceso es explícito: el owner de un proceso elige con quién compartirlo (usuarios individuales o grupos).
|
|
|
|
```
|
|
process_access
|
|
process_id uuid → processes.id
|
|
grantee_type 'user' | 'group'
|
|
grantee_id uuid (→ users.id si 'user', → user_groups.id si 'group')
|
|
permission 'view' | 'edit'
|
|
PRIMARY KEY (process_id, grantee_type, grantee_id)
|
|
```
|
|
|
|
Un proceso sin entradas en `process_access` es privado del owner. Un proceso compartido con el grupo "Equipo InQuality" es efectivamente de visibilidad de equipo. La tabla `process_access` es suficientemente flexible para Modelo A y Modelo B sin reestructurar.
|
|
|
|
### Procesos
|
|
|
|
```
|
|
processes
|
|
id uuid PK
|
|
name text
|
|
client text ← atributo descriptivo, NO entidad con acceso
|
|
currency text
|
|
frequency numeric
|
|
bpmn_xml text
|
|
owner_id uuid → users.id
|
|
created_by uuid → users.id
|
|
updated_by uuid → users.id
|
|
created_at timestamptz
|
|
updated_at timestamptz
|
|
```
|
|
|
|
Sin campo `visibility`. El acceso se determina completamente por `process_access` y por `platform_role` del usuario.
|
|
|
|
### Recursos — catálogo global
|
|
|
|
Este es el cambio más significativo de modelo. Los recursos dejan de ser por proceso y pasan a ser un catálogo global del equipo.
|
|
|
|
```
|
|
resources
|
|
id uuid PK
|
|
name text
|
|
type 'human' | 'system' | 'tool'
|
|
cost_per_hour numeric
|
|
created_by uuid → users.id
|
|
updated_by uuid → users.id
|
|
created_at timestamptz
|
|
updated_at timestamptz
|
|
```
|
|
|
|
Sin `process_id`. Sin control de acceso propio — los recursos son siempre visibles para todos los usuarios autenticados. Cualquier consultor puede crear, ver y editar recursos del catálogo.
|
|
|
|
Las asignaciones de recursos a actividades (`activity_resource_assignments`) referencian el `resource_id` del catálogo global.
|
|
|
|
### Actividades
|
|
|
|
Sin cambio de modelo respecto a Sprint 3. Se agrega `created_by`, `updated_by`, `created_at`, `updated_at`.
|
|
|
|
### Simulaciones
|
|
|
|
No tienen control de acceso propio — la regla de acceso se deriva del proceso al que pertenecen.
|
|
|
|
### Grupos de procesos y tags
|
|
|
|
Compartidos por todo el equipo. Sin campo de acceso. Sin `owner_id`.
|
|
|
|
---
|
|
|
|
## Interfaz AuthService — contrato obligatorio
|
|
|
|
Claude Code debe definir y respetar esta interfaz. No llamar a Supabase directamente desde componentes o stores.
|
|
|
|
```typescript
|
|
interface AuthUser {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
avatarUrl?: string
|
|
}
|
|
|
|
interface AuthService {
|
|
signInWithGoogle(): Promise<void>
|
|
signOut(): Promise<void>
|
|
getCurrentUser(): Promise<AuthUser | null>
|
|
onAuthStateChange(callback: (user: AuthUser | null) => void): () => void
|
|
}
|
|
```
|
|
|
|
La implementación `SupabaseAuthService` implementa esta interfaz y es la única que toca Supabase directamente. Cuando llegue Keycloak, se escribe `KeycloakAuthService` que implementa la misma interfaz. El resto de la app no cambia.
|
|
|
|
---
|
|
|
|
## Capa de repositorios — contrato obligatorio
|
|
|
|
Actualmente existe `src/persistence/repositories.ts` con Dexie. Se reemplaza por implementaciones Supabase manteniendo el mismo patrón de repositorios.
|
|
|
|
```typescript
|
|
// El contrato (interface) no cambia, la implementación sí
|
|
interface ProcessRepository {
|
|
getAll(): Promise<Process[]>
|
|
getById(id: string): Promise<Process | undefined>
|
|
save(process: Process): Promise<void>
|
|
delete(id: string): Promise<void>
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Los stores de Zustand llaman al repositorio. Los repositorios llaman a Supabase. Ningún componente toca Supabase directamente.
|
|
|
|
---
|
|
|
|
## Row-Level Security (RLS)
|
|
|
|
Las políticas de RLS de Supabase deben ser equivalentes a políticas PostgreSQL estándar. No usar features exclusivas de Supabase que no puedan recrearse en un PostgreSQL standalone cuando se migre a VPS.
|
|
|
|
### Helper function (crear una vez)
|
|
|
|
```sql
|
|
-- Retorna true si el usuario actual es platform_admin
|
|
CREATE OR REPLACE FUNCTION is_platform_admin()
|
|
RETURNS boolean AS $$
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM users
|
|
WHERE id = auth.uid() AND platform_role = 'platform_admin'
|
|
);
|
|
$$ LANGUAGE sql SECURITY DEFINER;
|
|
```
|
|
|
|
### Políticas para `processes`
|
|
|
|
```sql
|
|
-- Ver: platform_admin ve todo | owner ve los suyos | acceso explícito
|
|
CREATE POLICY "select_processes" ON processes FOR SELECT
|
|
USING (
|
|
is_platform_admin()
|
|
OR owner_id = auth.uid()
|
|
OR EXISTS (
|
|
SELECT 1 FROM process_access
|
|
WHERE process_id = processes.id
|
|
AND (
|
|
(grantee_type = 'user' AND grantee_id = auth.uid())
|
|
OR
|
|
(grantee_type = 'group' AND grantee_id IN (
|
|
SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
|
|
))
|
|
)
|
|
)
|
|
);
|
|
|
|
-- Insertar: cualquier usuario autenticado
|
|
CREATE POLICY "insert_processes" ON processes FOR INSERT
|
|
WITH CHECK (auth.uid() IS NOT NULL);
|
|
|
|
-- Actualizar: platform_admin | owner | quien tenga permiso 'edit'
|
|
CREATE POLICY "update_processes" ON processes FOR UPDATE
|
|
USING (
|
|
is_platform_admin()
|
|
OR owner_id = auth.uid()
|
|
OR EXISTS (
|
|
SELECT 1 FROM process_access
|
|
WHERE process_id = processes.id
|
|
AND permission = 'edit'
|
|
AND (
|
|
(grantee_type = 'user' AND grantee_id = auth.uid())
|
|
OR
|
|
(grantee_type = 'group' AND grantee_id IN (
|
|
SELECT group_id FROM group_memberships WHERE user_id = auth.uid()
|
|
))
|
|
)
|
|
)
|
|
);
|
|
|
|
-- Eliminar: solo el owner (platform_admin puede, por ser owner o por override)
|
|
CREATE POLICY "delete_processes" ON processes FOR DELETE
|
|
USING (is_platform_admin() OR owner_id = auth.uid());
|
|
```
|
|
|
|
### Políticas para `resources` (catálogo global)
|
|
|
|
```sql
|
|
-- Todos los autenticados pueden leer y escribir
|
|
CREATE POLICY "all_resources" ON resources
|
|
USING (auth.uid() IS NOT NULL)
|
|
WITH CHECK (auth.uid() IS NOT NULL);
|
|
```
|
|
|
|
### Políticas para `users`, `user_groups`, `group_memberships`, `process_access`
|
|
|
|
```sql
|
|
-- users: cualquier autenticado puede leer (para mostrar nombres), solo el propio usuario puede actualizar
|
|
CREATE POLICY "read_users" ON users FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
CREATE POLICY "update_own_user" ON users FOR UPDATE USING (id = auth.uid());
|
|
|
|
-- user_groups: todos los autenticados leen; platform_admin o owner del grupo puede modificar
|
|
CREATE POLICY "read_groups" ON user_groups FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
CREATE POLICY "manage_groups" ON user_groups FOR ALL
|
|
USING (is_platform_admin() OR created_by = auth.uid());
|
|
|
|
-- group_memberships: todos los autenticados leen; platform_admin o owner del grupo gestiona
|
|
CREATE POLICY "read_memberships" ON group_memberships FOR SELECT USING (auth.uid() IS NOT NULL);
|
|
CREATE POLICY "manage_memberships" ON group_memberships FOR ALL
|
|
USING (
|
|
is_platform_admin()
|
|
OR EXISTS (
|
|
SELECT 1 FROM user_groups
|
|
WHERE id = group_memberships.group_id AND created_by = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- process_access: owner del proceso o platform_admin gestiona los accesos
|
|
CREATE POLICY "manage_process_access" ON process_access FOR ALL
|
|
USING (
|
|
is_platform_admin()
|
|
OR EXISTS (
|
|
SELECT 1 FROM processes WHERE id = process_access.process_id AND owner_id = auth.uid()
|
|
)
|
|
);
|
|
CREATE POLICY "read_process_access" ON process_access FOR SELECT
|
|
USING (auth.uid() IS NOT NULL);
|
|
```
|
|
|
|
### Nota sobre migración a Modelo B
|
|
|
|
Para migrar de Modelo A a Modelo B (orgs autónomas), los únicos cambios son:
|
|
1. Eliminar o condicionar el bypass de `is_platform_admin()` en las políticas
|
|
2. Agregar tabla `organizations` y `org_memberships`
|
|
3. Ajustar las políticas para que el scope sea `org_id` en lugar de plataforma global
|
|
|
|
El schema de datos no cambia. Las tablas `user_groups`, `group_memberships` y `process_access` son compatibles con ambos modelos.
|
|
|
|
---
|
|
|
|
## UX — cambios de superficie
|
|
|
|
### Login
|
|
|
|
Pantalla de login antes de cualquier contenido. Dos flujos:
|
|
|
|
**Staff InQuality** → Botón "Iniciar sesión con Google". Solo cuentas `@inquality.com.py` obtienen acceso. Al autenticarse, la app verifica el dominio y asigna `platform_role = 'platform_admin'` si no existe registro previo en la tabla `users`.
|
|
|
|
**Guests** (clientes, consultores externos) → En Sprint 4 este flujo no está activado. La infraestructura (campo `platform_role = 'guest'`, tabla `users`) se construye lista para habilitarlo. El botón de magic link / email no aparece en la UI de Sprint 4. Se activa cuando haya un cliente real que incorporar.
|
|
|
|
Al completar login, la app redirige al workspace. Sin login, cualquier ruta protegida redirige a `/login`.
|
|
|
|
### Biblioteca (LibraryPage)
|
|
|
|
- Toggle de filtro: "Todos" / "Míos"
|
|
- Badge o ícono que indica visibilidad (privado 🔒 / compartido)
|
|
- En cada tarjeta de proceso: "Actualizado por [nombre]" + timestamp relativo
|
|
- Botón para cambiar visibilidad de un proceso propio
|
|
|
|
### ResourcesPanel (en workspace)
|
|
|
|
El panel de recursos ya no es "crear recursos para este proceso". Pasa a ser "seleccionar desde catálogo global" + "agregar nuevo al catálogo". El catálogo vive en una nueva sección de la app (Settings o panel dedicado).
|
|
|
|
### AppHeader
|
|
|
|
Mostrar avatar / nombre del usuario logueado. Botón de logout.
|
|
|
|
---
|
|
|
|
## Stack — cambios
|
|
|
|
| Agregado | Propósito |
|
|
| --- | --- |
|
|
| `@supabase/supabase-js` | Cliente Supabase (auth + db) |
|
|
| Variables de entorno `VITE_SUPABASE_URL` y `VITE_SUPABASE_ANON_KEY` | Configuración por entorno |
|
|
|
|
| Eliminado | Motivo |
|
|
| --- | --- |
|
|
| `dexie` | Reemplazado por Supabase PostgreSQL |
|
|
| `src/persistence/db.ts` | Reemplazado por cliente Supabase |
|
|
| Migraciones Dexie (v1-v4) | Ya no aplican |
|
|
|
|
Las variables de entorno deben existir en `.env.local` (ignorado por git) y documentarse en `.env.example`.
|
|
|
|
---
|
|
|
|
## Etapas tentativas
|
|
|
|
El orden importa — cada etapa habilita la siguiente.
|
|
|
|
| Etapa | Descripción |
|
|
| --- | --- |
|
|
| 1 | Supabase setup + AuthService + login page + Google OAuth |
|
|
| 2 | Schema PostgreSQL + migración de repositorios (procesos + grupos + tags) |
|
|
| 3 | Migración de actividades + recursos globales (nuevo modelo) |
|
|
| 4 | Migración de simulaciones + limpieza de Dexie |
|
|
| 5 | Modelo de privacidad: RLS + UI de visibilidad en biblioteca |
|
|
| 6 | Identidad en UI: header con usuario + "actualizado por" en biblioteca y workspace |
|
|
| 7 | ResourcesPanel: UX de catálogo global |
|
|
| 8 | Tests de integración + polish + validación E2E de auth |
|
|
|
|
Cada etapa tiene su propio `ETAPA_N_PROMPT.md` antes de arrancar. No improvisar orden ni scope entre etapas.
|
|
|
|
---
|
|
|
|
## Criterios de aceptación del sprint
|
|
|
|
- [ ] Login con Google Workspace (`@inquality.com.py`) funciona en desa y prod
|
|
- [ ] Ruta protegida — sin login no se accede a nada de la app
|
|
- [ ] Procesos se guardan y recuperan de Supabase (no de IndexedDB)
|
|
- [ ] Control de acceso funciona — un proceso sin `process_access` no lo ve otro usuario (salvo `platform_admin`)
|
|
- [ ] Recursos son globales — visibles para todos los usuarios autenticados
|
|
- [ ] En la biblioteca aparece "Actualizado por [nombre]" en cada proceso
|
|
- [ ] `AuthService` es una interface con implementación `SupabaseAuthService`
|
|
- [ ] No hay llamadas directas a Supabase fuera de repositorios y `AuthService`
|
|
- [ ] Dexie eliminado del proyecto (no solo sin uso — removido del package.json)
|
|
- [ ] Build limpio sin errores TypeScript
|
|
- [ ] Tests de dominio previos siguen verdes (simulación, ROI, PDF)
|
|
- [ ] Validación visual del director en cada etapa con output visual
|
|
|
|
---
|
|
|
|
## Riesgos y mitigaciones
|
|
|
|
**Google Workspace OAuth requiere configuración en Google Cloud Console.**
|
|
Acción: crear el proyecto OAuth antes de que Claude Code escriba una línea de código. El `client_id` y `client_secret` tienen que estar disponibles. Se configura también en Supabase Dashboard → Auth → Providers.
|
|
|
|
**El cambio de recursos globales rompe el UX actual completamente.**
|
|
Acción: el prompt de Etapa 7 (ResourcesPanel) debe incluir un mockup del nuevo UX aprobado antes de implementar. Aplicar Patrón C.5 (mockup como contrato visual).
|
|
|
|
**Múltiples etapas tienen riesgo de regresión en simulación/PDF.**
|
|
Acción: correr `npm run test` al final de cada etapa y verificar que los 561 tests previos siguen verdes. Si baja el count, parar y reportar antes de continuar.
|
|
|
|
---
|
|
|
|
## Fuera del scope de Sprint 4 — explícito
|
|
|
|
- Auditoría de cambios (audit log) — sprint dedicado futuro
|
|
- Keycloak / migración de auth provider — sprint futuro
|
|
- Offline mode / sync — roadmap
|
|
- Acceso de clientes externos — Fase 2
|
|
- Versionado de procesos — Fase 2
|
|
- Export a Excel — BACKLOG sin sprint
|
|
- Cualquier feature de PDF o simulación nueva
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
| Recurso | Propósito |
|
|
| --- | --- |
|
|
| `simulador-web/CLAUDE.md` | Reglas de implementación para Claude Code |
|
|
| `simulador-web/src/persistence/` | Código Dexie actual a reemplazar |
|
|
| `simulador-web/src/store/` | Stores Zustand que llaman a repositorios |
|
|
| `simulador-web/src/domain/types.ts` | Tipos de dominio (base del schema PostgreSQL) |
|
|
| `methodology/PATTERNS.md` | C.5 (mockup como contrato), C.7 (git diff preventivo) |
|
|
| `methodology/ANTI_PATTERNS.md` | AP.8 (adelanto silencioso) |
|
|
| Supabase docs — RLS | https://supabase.com/docs/guides/auth/row-level-security |
|
|
| Supabase docs — Google OAuth | https://supabase.com/docs/guides/auth/social-login/auth-google |
|