diff --git a/.claude/rules/INDEX.md b/.claude/rules/INDEX.md index 2693792a..c9ad9258 100644 --- a/.claude/rules/INDEX.md +++ b/.claude/rules/INDEX.md @@ -27,3 +27,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente. | 21 | [playgrounds.md](playgrounds.md) | Prototipos rapidos dentro de un artefacto padre — heredan entorno, no se indexan, no tienen repo propio | | 22 | [registry_first.md](registry_first.md) | Antes de escribir codigo en un artefacto: buscar en el registry, reutilizar si existe, delegar a `fn-constructor` si falta | | 23 | [fn_doctor.md](fn_doctor.md) | `fn doctor`: diagnostico read-only de artefactos, services, sync drift, uses_functions, unused — wrappers de funciones del registry | +| 24 | [feature_flags.md](feature_flags.md) | TBD: feature flags para mergear codigo incompleto sin romper master. Patrones por stack (Go/TS/Bash/Py), branch-by-abstraction, anti-patrones | diff --git a/.claude/rules/apps_tbd.md b/.claude/rules/apps_tbd.md index 359e7e94..4658224a 100644 --- a/.claude/rules/apps_tbd.md +++ b/.claude/rules/apps_tbd.md @@ -19,7 +19,20 @@ master ← siempre deployable 2. **Commits atomicos** por bloque logico (no WIP, no mezclar tipos). 3. **Tests obligatorios** antes de mergear (los que aplique al stack: ctest/go test/pytest/...). 4. **`merge --no-ff`** preserva la historia paralela. `git log --first-parent master` da la vista limpia. -5. **Feature flags** (no WIP) cuando una feature no cabe en una sola rama. Archivo: `dev/feature_flags.json`. +5. **Feature flags** (no WIP) cuando una feature no cabe en una sola rama. Archivo: `dev/feature_flags.json`. Detalle: `feature_flags.md`. + +### Que hacer cuando aparece WIP en el working tree + +Doctrina TBD: **master siempre desplegable**. Si tras implementar un issue queda codigo a medias en otros archivos (modificado pero no terminado), HAY DOS opciones legales: + +| Caso | Accion | +|---|---| +| WIP no relacionado al issue, pequeño, ya estable (ej. null-guards de un bug menor) | Incluirlo en el commit del issue **solo si compila + tests pasan**. Mencionarlo en el cuerpo del commit. | +| WIP relacionado al issue pero incompleto | Envolver en feature flag OFF (`enabled: false` en `dev/feature_flags.json`). Mergear codigo terminado y testeado. Activar flag en commit posterior. | +| WIP de otra feature distinta, no terminada | NO mergear con el issue. `git stash` o crear `issue/-...` para llevarlo aparte. NO romper master. | +| Pre-existing failing tests (no causados por la rama) | Documentar en cuerpo del commit/PR. Crear issue separado para el fix. NO bloquea merge si tu cambio no los introduce. | + +**Regla de oro:** ningun commit pusheado a master debe romper el deployment. Si el codigo no esta terminado pero compila + pasa tests, viaja detras de un flag OFF. Si rompe, no sale. ### Por que el registry esta exento diff --git a/.claude/rules/feature_flags.md b/.claude/rules/feature_flags.md new file mode 100644 index 00000000..d98b0171 --- /dev/null +++ b/.claude/rules/feature_flags.md @@ -0,0 +1,191 @@ +## Feature flags: enviar codigo incompleto a master sin romperlo + +Doctrina oficial de **trunk-based development**: master siempre desplegable. Cuando una feature no cabe en una sola rama corta, o cuando hay WIP que no esta terminado pero el resto si, **el codigo viaja detras de un flag OFF**. Asi master sigue verde y el codigo a medio terminar no llega a usuarios reales. + +Refs: [trunkbaseddevelopment.com/feature-flags/](https://trunkbaseddevelopment.com/feature-flags/), [trunkbaseddevelopment.com/branch-by-abstraction/](https://trunkbaseddevelopment.com/branch-by-abstraction/). + +### Cuando usar feature flag + +| Situacion | Accion | +|---|---| +| Feature multi-issue (`0015a`, `0015b`, `0015c`) que llevan dias | Cada sub-issue mergea con flag OFF. Ultimo sub-issue activa flag. | +| Refactor grande tipo "Branch by Abstraction" (ej. cambiar driver DB) | Crear abstraccion + impl nueva con flag. Eliminar antigua + flag al final. | +| Cambio con riesgo en produccion que necesita rollback rapido | Flag para apagar sin redeploy. | +| Despliegue gradual (un PC primero, luego todos) | Flag por PC/usuario/grupo. | +| WIP detectado al cerrar otra rama | Envolver el codigo a medias en flag OFF, mergear, terminar despues. | + +### Cuando NO usar feature flag + +- Bug fix autocontenido → mergear directo, sin flag. +- Refactor que cabe en una rama corta → directo. +- Docs, comments, type signatures → directo. +- Codigo que no compila o no pasa tests → **NO viaja a master, ni con flag**. Flag protege codigo terminado, no roto. + +### Flag != WIP + +- **WIP**: codigo a medias, no compila o no testea. NO va a master. +- **Flag**: codigo terminado y testeado, pero no expuesto al usuario. SI va a master. + +Si hay 80% terminado y 20% pendiente: completar al menos un slice vertical funcional (compila, pasa tests, se puede activar end-to-end), mergear con flag OFF, dejar el 20% para otra rama. NO mergear el 20% sin proteger. + +### Archivo de flags + +`dev/feature_flags.json` en la raiz del repo (registry o app). Formato canonico: + +```json +{ + "flags": { + "": { + "enabled": false, + "issue": "0063", + "description": "Descripcion 1 linea de la feature", + "added": "2026-05-08", + "enabled_at": null + } + } +} +``` + +Cuando se activa: cambiar `enabled: true` y rellenar `enabled_at` con fecha. Cuando la feature ya es estable y no necesita rollback (semanas/meses despues): borrar el flag y todas sus ramas condicionales del codigo. **Los flags caducan**; documentar fecha de revision para evitar que se acumulen. + +### Patron por stack + +#### Go (apps/services) + +Cargar flags al arrancar. Patron simple — hashmap en memoria + helper `Enabled(name)`: + +```go +// pkg/flags/flags.go (puro hasta donde se pueda) +package flags + +import ( + _ "embed" + "encoding/json" +) + +type Flag struct { + Enabled bool `json:"enabled"` + Issue string `json:"issue"` + Description string `json:"description"` +} + +type Flags struct{ Flags map[string]Flag `json:"flags"` } + +func Parse(b []byte) (Flags, error) { + var f Flags + err := json.Unmarshal(b, &f) + return f, err +} + +func (f Flags) Enabled(name string) bool { + flag, ok := f.Flags[name] + return ok && flag.Enabled +} +``` + +Uso: + +```go +if flags.Enabled("kanban-stickers") { + registerStickerRoutes(router) +} +``` + +Para flags en frontend embebido: serializar a `/api/flags` y leer desde el cliente (ver TS). + +#### TypeScript / React + +Inyectar en build (Vite) o exponer endpoint `/api/flags`: + +```ts +// src/flags.ts +let cache: Record | null = null; + +export async function loadFlags(): Promise> { + if (cache) return cache; + const res = await fetch("/api/flags"); + const data = await res.json(); + cache = Object.fromEntries(Object.entries(data.flags).map(([k, v]: [string, any]) => [k, !!v.enabled])); + return cache; +} + +export function isEnabled(name: string): boolean { + return !!(cache?.[name]); +} +``` + +Render condicional: + +```tsx +{isEnabled("kanban-stickers") && } +``` + +Para flags en build-time (constantes del bundle), usar `import.meta.env.VITE_FLAG_X` o un plugin Vite que reemplace simbolos. + +#### Bash / pipelines + +Lectura directa con `jq`: + +```bash +ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json) +if [ "$ENABLED" = "true" ]; then + run_new_path +else + run_legacy_path +fi +``` + +#### Python + +```python +import json +from pathlib import Path + +def flags() -> dict: + return json.loads(Path("dev/feature_flags.json").read_text())["flags"] + +def enabled(name: str) -> bool: + f = flags().get(name) + return bool(f and f.get("enabled")) + +if enabled("nuevo-pipeline"): + run_new() +else: + run_legacy() +``` + +### Branch by Abstraction (caso especial) + +Para cambios grandes (ej. swap iBatis → Hibernate, swap libreria, swap protocolo): + +1. **Abstraer**: crear interfaz que envuelve la implementacion antigua. Master sigue verde con la antigua. Mergear. +2. **Implementar nueva**: bajo la misma interfaz, detras de flag OFF. Tests para ambas. Mergear. +3. **Activar**: flip flag a ON en commit pequeño. Si rompe, flip OFF de inmediato. +4. **Eliminar antigua**: borrar codigo legacy + flag + abstraccion. Mergear. + +Cada paso es un merge corto, master nunca esta roto, hay rollback en cada punto. + +### Reglas operativas + +- **Un flag = un proposito**. Si necesitas dos toggles independientes, usa dos flags. +- **Flag bool por defecto**. Si necesitas A/B/C, sigue siendo bool por nombre (`my-feature-v2`, `my-feature-v3`). +- **Tests con flag ON y OFF**. CI corre ambos paths cuando el flag toca codigo critico. +- **Documenta en el issue**: que flag protege que codigo, cuando se va a activar, cuando se va a borrar. +- **No anidar flags**. Si una rama esta detras de dos flags, simplifica. +- **Borra el flag**. Cuando la feature lleva semanas activa sin rollback, eliminar el flag es trabajo real, no opcional. + +### Anti-patrones + +| Anti-patron | Por que es malo | +|---|---| +| `if (flag) { ... } else { ... }` esparcido por 30 archivos | Imposible de borrar. Usar inyeccion / strategy pattern. | +| Flag que lleva 6 meses ON sin borrar | Deuda tecnica. Borrar el flag y simplificar. | +| Flag para WIP que no compila | Master roto. Eso no es flag, es WIP — no debe estar en master. | +| Flag condicional sobre tipos / esquemas DB | Migrations son irreversibles. No se "apaga" una columna. Usar branch-by-abstraction sobre la lectura/escritura, no sobre el schema. | +| Flag con nombre del autor o del issue (`lucas-experiment`, `flag-0063`) | Sin contexto al releerlo. Nombrarlo por la feature: `kanban-stickers`. | + +### Comandos relacionados + +- `/git-branch` — crea rama desde master. +- `/git-push` — merge --no-ff + push. +- Para registrar / activar un flag: editar `dev/feature_flags.json` directamente y commitear con el codigo correspondiente. No hay CLI dedicada todavia.