
Del read-only al primer writer controlado en IXATU Operations
Este artículo es la narrativa pública del documento de trabajo interno correspondiente. Conserva hallazgos, semántica, fallos reales y límites; omite o generaliza únicamente identificadores operativos privados que no aportan valor editorial.
IXATU Operations ha cruzado la frontera entre consultar el sistema legacy y modificarlo de forma controlada.
La primera capability de escritura se ha implementado deliberadamente sobre un único campo —
Contactos.[Tfno principal]— para demostrar el patrón completo: autenticación, autorización, separación reader/writer, permisos SQL mínimos, concurrencia optimista, transacción, auditoría, UI humana, CI/CD y aceptación real enIXATU_DEV.PROD permanece sin writer configurado.
El valor estratégico de
v0.9.0no es «poder cambiar un teléfono», sino disponer del primer carril seguro y reusable de escritura de Operations sobre el sistema actual.
Índice
- [[#1. Estado ejecutivo]]
- [[#2. Contexto: de lectura a escritura]]
- [[#3. Qué se ha construido en v0.9.0]]
- [[#4. Por qué empezar por un único campo]]
- [[#5. Discovery LPD y contrato legacy]]
- [[#6. Arquitectura del writer]]
- [[#7. Capas de seguridad]]
- [[#8. Autenticación, autorización y permisos SQL no son lo mismo]]
- [[#9. Alta de usuarios autorizados en DEV]]
- [[#10. Reader y writer SQL]]
- [[#11. Concurrencia: rowversion, ETag e If-Match]]
- [[#12. Transacción y auditoría]]
- [[#13. Semántica del teléfono y
Sin informar]] - [[#14. Entornos: localhost, DEV y PROD]]
- [[#15. Flujo de implementación, merge, Bake y Deploy DEV]]
- [[#16. Evidencias y pruebas realizadas]]
- [[#17. Hallazgos y decisiones significativas]]
- [[#18. Patrón reusable para futuros writers]]
- [[#19. Qué no debemos generalizar todavía]]
- [[#20. Roadmap global recorrido]]
- [[#21. Seis frentes aprobados desde este punto]]
- [[#22. Frente 2 — próximo objetivo: formulario Contacto]]
- [[#23. Evolución hacia operaciones multisuministro]]
- [[#24. CLI y agentes IA]]
- [[#25. Paso futuro a PROD]]
- [[#26. Reglas operativas y mnemotécnicas]]
- [[#27. Riesgos y deuda conocida]]
- Evidencia operativa (sin identificadores privados)
1. Estado ejecutivo
A fecha 2026-09-18, IXATU Operations dispone en DEV de su primer writer humano operativo end-to-end.
La capability permite editar únicamente:
dbo.Contactos.[Tfno principal]
El flujo se ha probado con un contacto de prueba real de IXATU_DEV (un contacto de prueba de DEV) y se ha validado:
- login mediante Microsoft Entra ID;
- autorización por Object ID;
- acceso Web → API mediante gate interno;
GETfresco del valor y su versión;PATCHprotegido porIf-Match;- escritura real en Azure SQL DEV;
- rechazo de una escritura obsoleta mediante HTTP
412; - auditoría sin almacenar PII;
- restauración exacta del valor original;
- UI humana con
Editar,Guardar,Cancelary gestión de conflicto; - despliegue automatizado de DEV desde
main; - acceso de prueba habilitado para el equipo mediante allowlist.
Estado de entornos:
| Área | DEV | PROD |
|---|---|---|
| Lectura Operations | Operativa | Operativa |
| Entra / autenticación | Operativa | Operativa |
| Writer SQL Operations | Sí | No |
| UI de edición teléfono | Sí | No promovida/configurada para escritura |
| Allowlist de writers | Sí | No |
| Audit de writes | Sí | No aplica |
| Escrituras Operations | Permitidas y controladas | Bloqueadas por diseño |
| Fase actual | Estabilización / aceptación | Lectura / sin writer |
La regla vigente es:
PROD_MUTATIONS = 0No se configura ni se habilita escritura en producción durante el Release Gate de
v0.9.0.
2. Contexto: de lectura a escritura
Operations se diseñó para sustituir Access de forma incremental, sin migrar primero todo el modelo legacy.
Principio central:
Primero sustituir el comportamiento de Access; después sustituir su modelo de datos.
Durante las versiones anteriores, Operations fue ganando capacidad de lectura sobre el sistema real:
flowchart LR
A[Access + Azure SQL legacy] --> B[ACL / adapters]
B --> C[FastAPI]
C --> D[Operations Web]
D --> E[Usuarios]
Hasta v0.8.x, la dirección era fundamentalmente:
Azure SQL
↓
Operations
↓
Usuario
Con v0.9.0 aparece por primera vez el camino inverso:
Usuario
↓
Operations
↓
Azure SQL DEV
Esto cambia la naturaleza del producto.
Operations deja de ser únicamente una interfaz de consulta y se convierte en una aplicación capaz de ejecutar operaciones de negocio controladas sobre el legacy actual.
3. Qué se ha construido en v0.9.0
La implementación se dividió deliberadamente en dos slices.
Slice A — Write Foundation + Backend
Issues / PRs:
- la Issue correspondiente
- la PR correspondiente
Objetivo:
- construir el carril de escritura;
- mantener separado el read-side existente;
- habilitar autenticación/autorización;
- crear un principal SQL writer mínimo;
- implementar concurrencia y audit;
- validar una escritura real en DEV.
Resultado:
backend + seguridad + SQL + audit + deployment = PASS
Slice B — UI humana
Issues / PRs:
- la Issue correspondiente
- la PR correspondiente
Objetivo:
- exponer el writer desde la ficha de cliente;
- no convertir toda la ficha en formulario;
- reutilizar exactamente la capability backend;
- validar la experiencia humana.
Resultado:
UI → API → writer → IXATU_DEV = PASS
Release Gate actual
Issue:
#79 — v0.9.0 Release Gate — estabilización y aceptación de equipo en DEV
En esta fase:
- no se añaden nuevas capabilities;
- el equipo prueba el writer en DEV;
- se recogen incidencias;
- sólo se corrigen defectos bloqueantes o pequeñas fricciones de
v0.9.0; - PROD permanece fuera de alcance;
- el bump/tag/release se realiza al final del gate.
4. Por qué empezar por un único campo
El teléfono principal se eligió deliberadamente como writer mínimo vertical, no porque fuese el campo más importante del negocio.
La pregunta que se quería responder era:
¿Podemos escribir de Operations a Azure SQL de forma segura, auditable, concurrente, desplegable y comprensible?
El primer writer debía reducir al mínimo las variables de negocio para concentrar el riesgo en la infraestructura de escritura.
El teléfono permitió probar:
identidad
+ autorización
+ API
+ permisos SQL
+ transacción
+ rowversion
+ audit
+ UI
+ CI/CD
+ recuperación
sin mezclar todavía:
- propagaciones legacy complejas;
- formularios con múltiples campos;
- reglas de negocio cruzadas;
- operaciones masivas;
- relaciones multisuministro;
- cambios bancarios;
- cutover de Access.
Regla aprendida
El primer writer debe ser pequeño en negocio, pero completo en arquitectura.
El valor de esta implementación está en el patrón demostrado, no en la cantidad de columnas editables.
5. Discovery LPD y contrato legacy
Antes de escribir se realizó discovery del formulario Access representativo y del contrato SQL.
LPD — Legacy Product/Process Discovery
Se inspeccionó un Access de producción representativo en copia temporal segura.
Hallazgos para Tfno principal:
- el formulario
Contactosestá vinculado aContactos; Tfno principales un TextBox bound;- el control está habilitado y no bloqueado;
- no presenta
ValidationRule; - no presenta
InputMask; - no tiene eventos específicos de guardado;
- no se encontró VBA/macros especiales asociados al cambio;
- Access usa el guardado bound normal;
- no se identificaron side effects adicionales específicos del teléfono.
Veredicto:
LPD = PASS
confidence = HIGH
Contrato SQL
Campo:
dbo.Contactos.[Tfno principal] nvarchar(255) NULL
Identidad:
dbo.Contactos.Id_Contacto int identity primary key
Control de versión legacy:
dbo.Contactos.SSMA_TimeStamp timestamp / rowversion
No se encontraron:
- triggers relevantes;
- checks específicos del teléfono;
- reglas de normalización.
Hallazgo importante: no todos los campos serán iguales
El email se dejó fuera deliberadamente.
Durante discovery aparecieron indicios de comportamiento/propagación histórica que impiden asumir:
email = otra columna simple
Esto confirma el valor de LPD.
No convertir la tabla
Contactosen un CRUD genérico.Cada capability de escritura debe demostrar antes qué comportamiento legacy está sustituyendo.
6. Arquitectura del writer
Diagrama simplificado:
flowchart TD
U[Usuario] --> EA[Microsoft Entra / Easy Auth]
EA --> W[Admin Web DEV]
W --> N[Nginx]
N -->|identity headers + internal gate| API[FastAPI]
API --> AUTH[WriteAuthorizer]
AUTH --> UC[UpdatePrimaryPhone]
UC --> ET[ETag / rowversion]
ET --> SQLW[Dedicated SQL Writer]
SQLW --> DB[(IXATU_DEV)]
SQLW --> AUD[(OperationsWriteAudit)]
Vista por responsabilidades:
Browser
↓
Entra / Easy Auth
↓
Nginx internal gate
↓
FastAPI / WriteAuthorizer
↓
Use case
↓
ETag / rowversion
↓
Dedicated SQL writer
↓
Contactos + Audit
Cada capa responde a una pregunta distinta:
| Capa | Pregunta |
|---|---|
| Entra | ¿Quién eres? |
| Allowlist / WriteAuthorizer | ¿Puedes escribir? |
| Gate Web→API | ¿La identidad llega por el caller esperado? |
| Use case | ¿La operación es válida? |
| ETag / rowversion | ¿Editas la misma versión que leíste? |
| SQL grants | ¿Qué puede modificar técnicamente Operations? |
| Transacción | ¿El cambio queda íntegro? |
| Audit | ¿Quién hizo qué operación y con qué resultado? |
7. Capas de seguridad
La seguridad no depende de un único mecanismo.
7.1 Microsoft Entra ID
Entra autentica a la persona.
La identidad relevante para autorización es el Object ID estable, no:
- email;
- nombre;
- alias;
- UPN mostrado en UI.
7.2 Allowlist de writers
Variable GitHub:
IXATU_DEV_WRITE_ALLOWED_PRINCIPAL_IDS
Se inyecta en la API como:
IXATU_WRITE_ALLOWED_PRINCIPAL_IDS
Contiene Object IDs separados por comas.
Ejemplo conceptual:
uuid-jesus,uuid-xabier,uuid-izaskun,...
A fecha del documento, se ha habilitado el piloto para los seis miembros acordados del equipo:
- Xabier;
- Izaskun;
- Jesús;
- Elena;
- Odei;
- Chechu / Joseramón.
7.3 Fail closed
La autorización está diseñada para fallar cerrada.
Un Object ID no autorizado produce:
HTTP 403
Además, el parser actual de allowlist tiene una característica operativa relevante:
Si la variable contiene un UUID mal formado, la construcción de la allowlist falla y el resultado efectivo es una lista vacía.
Consecuencia:
UUID inválido en configuración
→ nadie puede escribir
→ fallo seguro
Esto obliga a cuidar la actualización de la variable.
7.4 Gate interno Web → API
La API no confía simplemente en headers enviados por el navegador.
Nginx:
- elimina headers de identidad inbound no confiables;
- inyecta server-side la identidad proporcionada por Easy Auth;
- añade un secreto interno
X-Ixatu-Write-Gate; - no devuelve ese secreto al browser;
- desactiva forwarding indiscriminado de headers en la ruta sensible.
La API exige:
gate válido
+ provider = aad
+ Object ID válido
+ Object ID allowlisted
Esto reduce el riesgo de que otro caller dentro del Container Apps Environment fabrique headers y escriba directamente.
7.5 API interna
La Container App de FastAPI usa ingress interno.
La Web es el ingress externo y el proxy autorizado.
7.6 SQL privilege minimization
La API writer no usa:
db_owner
db_datawriter
db_datareader
El principal writer recibe únicamente los permisos necesarios para su capability.
8. Autenticación, autorización y permisos SQL no son lo mismo
Esta distinción es esencial.
flowchart TD
A[Cuenta Entra] --> B{Autenticado?}
B -->|No| X[No hay identidad writer]
B -->|Sí| C{Object ID allowlisted?}
C -->|No| D[403]
C -->|Sí| E[Operations acepta la operación]
E --> F[Principal SQL técnico]
F --> G{¿Tiene grant exacto?}
G -->|No| H[SQL rechaza]
G -->|Sí| I[Write]
Tener cuenta Entra no equivale a poder escribir
Entra responde:
Esta persona es X.
Operations responde después:
X está o no autorizada para esta capability.
Tener usuario SQL personal no es requisito
Los usuarios humanos no escriben desde Operations usando:
xabier_dev
izaskun_dev
jesus_dev
...
Operations utiliza su principal técnico.
Usuarios SQL humanos existentes
En IXATU_DEV existen actualmente, entre otros:
chechu_dev
izaskun_dev
jesus_dev
odei_dev
xabier_dev
con:
db_datareader
db_datawriter
También existe:
i3code → db_owner
Estos accesos son independientes de Operations y son considerablemente más amplios.
Hallazgo de gobierno:
Los usuarios SQL personales pueden ser útiles para SSMS/administración DEV, pero no deben convertirse en el mecanismo de autorización de la aplicación.
A futuro conviene auditar su necesidad y privilegios.
9. Alta de usuarios autorizados en DEV
Paso 1 — obtener Object ID Entra
Ejemplo:
az ad user show \
--id [email protected] \
--query id \
-o tsv
Para el propio usuario:
az ad signed-in-user show --query id -o tsv
Paso 2 — actualizar allowlist
gh variable set IXATU_DEV_WRITE_ALLOWED_PRINCIPAL_IDS \
--repo IXATU/ixatu-operations \
--body '<uuid-1>,<uuid-2>,<uuid-3>'
Los Object IDs no son contraseñas, pero no deben difundirse innecesariamente.
Paso 3 — desplegar main
gh workflow run deploy-dev.yml \
--repo IXATU/ixatu-operations \
--ref main
El workflow actualiza la configuración efectiva de la API.
Paso 4 — prueba sin escritura
Para comprobar autorización sin mutar datos:
- entrar en DEV;
- autenticar mediante Entra;
- abrir el contacto de prueba;
- pulsar
Editar; - verificar que aparece el input;
- pulsar
Cancelar.
Si el usuario no está autorizado:
No tienes permiso para editar este campo.
No añadir a la allowlist
No deben añadirse:
ixatu_operations_dev_ro
ixatu_operations_dev_phone_writer
porque son usuarios SQL, no identidades Entra humanas.
Tampoco deben añadirse «Claude», «ChatGPT», «Cursor» o agentes equivalentes como si fueran personas.
Si en el futuro un agente necesita escritura, tendrá una identidad técnica propia.
10. Reader y writer SQL
La separación de credenciales es un invariante.
Reader
Variables:
IXATU_SQL_*
Principal:
ixatu_operations_dev_ro
Protección observada:
db_denydatawriter
Además, el adapter de lectura usa:
ApplicationIntent=ReadOnly
ApplicationIntent=ReadOnlyes una intención del cliente, no una barrera de permisos. La defensa real es SQL privilege minimization.
Writer
Variables:
IXATU_WRITE_SQL_*
Principal:
ixatu_operations_dev_phone_writer
Permisos efectivos para el primer writer:
SELECT:
- Id_Contacto
- Tfno principal
- SSMA_TimeStamp
UPDATE:
- Tfno principal
INSERT:
- dbo.OperationsWriteAudit
No tiene:
db_owner
db_datawriter
db_datareader
UPDATE general de Contactos
INSERT en Contactos
DELETE
ALTER
CONTROL
Principio
Un writer no recibe permisos para la tabla; recibe permisos para la capability.
11. Concurrencia: rowversion, ETag e If-Match
El legacy dispone de:
SSMA_TimeStamp
que SQL Server trata como rowversion.
Operations lo expone como un ETag opaco.
Lectura fresca antes de editar
GET /customers/{customer_id}/contact/primary-phone
Respuesta conceptual:
primaryPhone = valor
ETag = "version-A"
La UI no usa el teléfono previamente cargado en la ficha como snapshot de concurrencia.
Al pulsar Editar hace un GET específico fresco.
Guardado
PATCH /customers/{customer_id}/contact/primary-phone
If-Match: "version-A"
SQL
Patrón conceptual:
UPDATE dbo.Contactos
SET [Tfno principal] = ?
OUTPUT inserted.[Tfno principal], inserted.SSMA_TimeStamp
WHERE Id_Contacto = ?
AND SSMA_TimeStamp = ?;
Resultado
Si la fila sigue en la versión A:
1 row
→ 200
→ nuevo ETag B
Si alguien la cambió:
0 rows
→ existe el contacto pero rowversion cambió
→ 412 Precondition Failed
Esto evita el lost update.
Regla
Nunca sobrescribir silenciosamente un cambio concurrente.
La UI ante 412:
- conserva el borrador;
- informa del conflicto;
- invalida el ETag;
- obliga a
Recargar valor; - no reintenta automáticamente.
12. Transacción y auditoría
Transacción
El writer usa:
autocommit = False
y commit/rollback explícitos.
La infraestructura SQL inicial se corrigió para utilizar:
SET XACT_ABORT ON
BEGIN TRY
BEGIN TRANSACTION
...
COMMIT
BEGIN CATCH
ROLLBACK
THROW
Audit
Tabla:
dbo.OperationsWriteAudit
Registra metadatos operativos, no datos de negocio sensibles.
Incluye conceptualmente:
- timestamp UTC;
- actor Object ID;
- operación;
contact_id;- request/correlation ID;
- resultado.
Operación:
contact.primary_phone.update
No PII
No deben almacenarse en audit/logs:
- teléfono anterior;
- teléfono nuevo;
- nombre;
- NIF;
- email;
- dirección;
- CUPS;
- IBAN.
Evidencia real
Primera prueba backend:
success
conflict
success
correspondiente a:
write
stale write rechazado
restore
Prueba humana UI:
success
success
correspondiente a:
primera escritura UI
restauración del valor original
13. Semántica del teléfono y Sin informar
El legacy acepta:
NULL
o
nvarchar <= 255
No se ha introducido:
- regex española;
- normalización de
+34; - eliminación automática de espacios;
- formato telefónico;
- truncado silencioso.
La UI conserva el valor literal.
Sin informar
Distingue:
"" → cadena vacía
NULL → valor no informado
La opción actual Sin informar envía:
{"primaryPhone": null}
Esto refleja la semántica SQL real.
Posible mejora UX a observar durante estabilización:
Sin teléfonoBorrar teléfono
No debe cambiarse la semántica sin decidirlo explícitamente.
14. Entornos: localhost, DEV y PROD
Localhost
El localhost no es por sí mismo un entorno de datos.
Depende de la configuración.
La aceptación local usada en Slice B fue sintética, con Playwright interceptando respuestas como:
SYN-OLD
SYN-FRESH
SYN-WRITER
No hubo escritura SQL.
El docker-compose.yml local actual no inyecta por defecto:
IXATU_WRITE_SQL_*
IXATU_WRITE_ALLOWED_PRINCIPAL_IDS
IXATU_WRITE_INTERNAL_GATE
Por tanto:
Localhost sirve para validar UI, contrato HTTP y estados de error con fixtures, no para demostrar el writer real.
DEV
URL de referencia:
la URL pública de la aplicación en Azure Container Apps
Base:
ixatu-dev.database.windows.net
IXATU_DEV
DEV es el laboratorio de escritura.
Aquí se validan:
- Easy Auth;
- Object ID;
- gate;
- writer SQL;
- SQL real;
- audit;
- concurrencia;
- comportamiento humano.
PROD
URL de referencia:
la URL pública de la aplicación en Azure Container Apps
Base:
ixatu.database.windows.net
IXATU
PROD dispone del pilot de lectura protegido, pero no se ha configurado el writer v0.9.0.
No existen todavía como capability promovida de escritura PROD:
- writer SQL de teléfono;
- allowlist de writers PROD;
- grants writer;
- audit writer asociado a esta capability;
- deployment de write habilitado.
DEV y PROD son entornos separados. Nunca se «copia» informalmente la configuración writer de DEV a PROD.
15. Flujo de implementación, merge, Bake y Deploy DEV
Flujo habitual de feature
flowchart TD
A[main estable] --> B[feature branch desde SHA exacto]
B --> C[TDD / implementación]
C --> D[Tests locales]
D --> E[Playwright sintético]
E --> F[Docker Buildx Bake]
F --> G[Draft PR]
G --> H[PR CI]
H --> I[Auditoría Director]
I --> J[Ready]
J --> K[Squash merge main]
K --> L[Push main]
L --> M[Deploy DEV automático]
M --> N[Human Acceptance DEV]
N --> O[Issue closure / Release Gate]
CI del PR
Los checks actuales incluyen:
Python
→ Ruff
→ pytest
→ wheel
Admin Web
→ npm ci
→ typecheck
→ build
→ Playwright synthetic browser tests
Docker
→ docker buildx bake
Deploy DEV
flowchart TD
A[push main] --> B[GitHub Actions deploy-dev.yml]
B --> C[OIDC login Azure]
C --> D[docker buildx bake --push]
D --> E[ACR DEV<br/>tag = commit SHA]
E --> F[Update API Container App]
E --> G[Update Web Container App]
F --> H[Stable revision]
G --> H
H --> I[Public Web smoke]
I --> J[/api/health]
J --> K[Swagger/OpenAPI DEV checks]
Características:
- autenticación GitHub→Azure mediante OIDC;
- no se almacena client secret de deployment;
- imágenes etiquetadas por commit SHA;
- ACR DEV;
- actualización idempotente de Container Apps;
- secrets actualizados antes de nueva revisión;
min replicas = 0,max replicas = 1;- modo de revisión single;
- smoke a través de la Web pública.
Bake
docker buildx bake es parte del gate de reproducibilidad.
No sólo se comprueba:
"el TypeScript compila"
sino:
"las imágenes reales de API y Web se pueden construir"
Feature branch deployment
No es el camino normal.
Durante Slice A se usó temporalmente una credencial OIDC exacta de feature branch porque era necesario validar infraestructura writer antes del merge.
La credencial:
github-feature-75-write-foundation
se eliminó antes de integrar.
Slice B volvió al patrón normal:
feature
→ CI/local
→ merge
→ deploy DEV desde main
Lección
No abrir confianza OIDC de feature branches salvo que exista un motivo operativo explícito y temporal.
16. Evidencias y pruebas realizadas
Backend
455 pytestPASS.- Ruff PASS.
- Docker Bake PASS.
- security tests PASS.
- writer SQL tests PASS.
- API contract tests PASS.
UI
85 PlaywrightPASS tras Slice B.- desktop 1440 px PASS.
- mobile 390 px PASS.
- no overflow relevante.
- typecheck PASS.
- Vite build PASS.
Write E2E backend real
Fixture DEV:
un contacto de prueba de DEV
Se comprobó además que no existía en PROD como fixture equivalente usado para la prueba.
Secuencia:
GET snapshot
→ PASS
PATCH synthetic
→ 200
PATCH con ETag antiguo
→ 412
restore exacto
→ 200
GET final
→ original restaurado
Audit:
success
conflict
success
Human Acceptance UI
Secuencia:
Editar
→ Cancelar
→ PASS / no write
Editar
→ Guardar valor sintético
→ PASS
SQL verification
→ ui_write_confirmed = 1
Editar
→ restaurar original
→ PASS
SQL verification
→ ui_write_confirmed = 0
Audit adicional:
success ← UI write
success ← restore
Resultado:
Primer writer humano de Operations operativo end-to-end en IXATU_DEV.
17. Hallazgos y decisiones significativas
17.1 El writer no necesita usuarios SQL humanos
Operations no autentica en SQL como:
jesus_dev
xabier_dev
izaskun_dev
Los usuarios trabajan con Entra.
La aplicación utiliza un principal técnico.
17.2 Los usuarios SQL personales son otro canal
Los usuarios _dev existentes con db_datareader + db_datawriter son útiles potencialmente para SSMS o administración, pero no son parte del modelo de seguridad de la aplicación.
17.3 Reader y writer deben permanecer separados
Nunca ampliar el reader existente para «aprovecharlo» y escribir.
17.4 Permiso por capability, no por tabla
El writer sólo puede modificar la columna necesaria.
17.5 La UI no decide permisos
No se implementa lógica como:
si email == xabier@...
mostrar write
La API es la autoridad.
17.6 Un GET de ficha no es suficiente para editar
El snapshot de edición debe ser fresco y específico de la capability.
17.7 412 es éxito del sistema de protección
Un 412 Precondition Failed en stale write significa:
la protección de concurrencia ha funcionado
no un fallo del writer.
17.8 Audit no debe convertirse en una copia de los datos
Audit registra operación, actor y resultado; no los valores sensibles.
17.9 El primer writer fue costoso a propósito
Se pagó el coste de construir el carril reusable:
- auth;
- authz;
- gate;
- writer credentials;
- grants;
- ETag;
- audit;
- deployment;
- tests.
Los siguientes writers deben reutilizar esta infraestructura.
17.10 Versionar no es lo mismo que terminar de programar
Aunque la capability ya funciona, v0.9.0 permanece en Release Gate.
El bump se realiza después de estabilización del equipo.
18. Patrón reusable para futuros writers
Separación recomendada:
Rail reusable
Microsoft Entra
Object ID
WriteAuthorizer
internal Web→API gate
reader/writer credential split
SQL least privilege
ETag / If-Match
transaction conventions
audit
sanitized errors
GitHub Actions wiring
DEV-first human gates
Capability específica
Para teléfono:
route primary-phone
DTO primaryPhone
validation null|string<=255
UPDATE Tfno principal
column grant
PrimaryPhoneField UI
tests de teléfono
Fórmula conceptual
NUEVO WRITER
=
LPD
+ capability específica
+ use case
+ SQL mínimo
+ permisos mínimos
+ concurrencia
+ audit
+ tests
+ aceptación DEV
Diagrama:
flowchart LR
LPD[LPD] --> DOM[Use case]
DOM --> API[HTTP capability]
API --> AUTH[Rail de seguridad existente]
AUTH --> SQL[SQL exacto]
SQL --> GRANT[Grant mínimo]
GRANT --> TEST[Test]
TEST --> DEV[Human Acceptance DEV]
19. Qué no debemos generalizar todavía
No construir aún:
GenericRepository.update(table, fields)
GenericCRUD<Contactos>
GenericWriter
Tampoco convertir todo Contactos en editable automáticamente porque exista un primer writer.
Secuencia recomendada:
writer 1
→ aprendemos
writer 2
→ identificamos repetición real
writer 3
→ abstraemos sólo lo demostrado
No abstraer la incertidumbre.
20. Roadmap global recorrido
El siguiente mapa es funcional y arquitectónico. Para el ledger exacto de tags/releases debe consultarse GitHub Releases.
flowchart TD
V01[v0.1.x<br/>Walking Skeleton + Azure DEV + CI/CD] --> V02[Infra / pilot protegido / delivery]
V02 --> V03[Customer workspace / discovery UI]
V03 --> V04[Cliente + Suministros<br/>lectura real]
V04 --> V05[Contactos<br/>lectura operativa]
V05 --> V06[Ofertas + Contrato Comercial]
V06 --> V07[Home / Informes / Vencimientos]
V07 --> V08[Gestiones + contextos frecuentes]
V08 --> V09[v0.9.0<br/>Primer writer controlado]
V09 --> NOW[Release Gate DEV<br/>ESTAMOS AQUÍ]
NOW --> F2[Formulario Contacto]
F2 --> F3[Patrón reusable writers]
F3 --> F4[CLI / agentes]
F4 --> F5[Operaciones multisuministro]
F5 --> F6[Pilot writer PROD]
Lectura resumida
Etapa 1 — plataforma base
Objetivos ya recorridos:
- repositorio y arquitectura;
- FastAPI;
- React/Vite;
- Docker;
- Buildx/Bake;
- Azure Container Apps;
- ACR;
- GitHub Actions;
- OIDC;
- separación DEV/PROD;
- smokes;
- release gates.
Etapa 2 — sustitución progresiva de consultas Access
Objetivos recorridos:
- Home;
- clientes;
- suministros;
- contactos;
- contratos;
- ofertas;
- vencimientos;
- informes;
- gestiones.
Etapa 3 — primera escritura
Objetivo actual:
Operations read-only
→ Operations read/write controlado en DEV
Etapa 4 — formularios
Próxima frontera:
1 capability de 1 campo
→ formulario coherente
Etapa 5 — operaciones de negocio compuestas
Después:
1 entidad
→ varias entidades
→ operaciones multisuministro
Etapa 6 — cutover progresivo
Mucho más adelante:
writer estabilizado DEV
→ pilot PROD
→ convivencia
→ desactivar comportamiento Access equivalente
→ estabilizar
→ migración/modelado posterior
21. Seis frentes aprobados desde este punto
Los seis frentes quedan aprobados como marco de dirección.
Frente 1 — cerrar bien v0.9.0
Estado:
ACTUAL
Objetivo:
- estabilización de equipo en DEV;
- validar autorización de usuarios;
- recoger feedback;
- fixes mínimos;
- bump
0.9.0; - CI;
- Deploy DEV final;
- tag + GitHub Release.
No añadir nuevas capabilities durante este gate.
Frente 2 — formulario Contacto
Estado:
SIGUIENTE
Objetivo:
- discovery LPD de campos del formulario;
- identificar validaciones, relaciones y side effects;
- definir bounded form;
- guardar varios campos coherentes;
- una estrategia de concurrencia;
- una transacción consistente;
- audit de operación.
No asumir que todos los campos son tan simples como teléfono.
Frente 3 — consolidar patrón reusable de writers
Objetivo:
separar definitivamente:
infra reusable
vs
lógica específica
Sin construir un framework genérico prematuro.
Frente 4 — CLI / agentes IA
Objetivo:
permitir que automatizaciones autorizadas utilicen las mismas capabilities que la UI.
Regla:
agente
→ API
→ misma auth/authz
→ mismo ETag
→ mismo audit
No:
agente
→ SQL directo
Frente 5 — operaciones multisuministro
Objetivo:
operaciones de alto valor como:
- cambios coordinados;
- acciones sobre varios suministros;
- posibles cambios bancarios;
- operaciones masivas con previsualización;
- audit de operación.
Debe diseñarse como operación de negocio, no como un bucle de UPDATE.
Frente 6 — primer writer PROD
Objetivo futuro.
Requiere gate separado:
- principal writer PROD;
- grants mínimos;
- allowlist PROD;
- audit;
- backup/recovery;
- deployment;
- acceptance;
- convivencia con Access;
- rollback;
- seguimiento.
No copiar DEV automáticamente.
22. Frente 2 — próximo objetivo: formulario Contacto
La siguiente pregunta no es:
«¿Qué campo editamos ahora?»
La pregunta correcta es:
¿Cuál es el primer conjunto coherente de datos de Contacto que debe guardarse como una operación única?
Discovery previo
flowchart TD
A[Formulario Access Contactos] --> B[Controls / bindings]
B --> C[Validation rules]
C --> D[VBA / macros / queries]
D --> E[Side effects]
E --> F[SQL schema]
F --> G[Relations]
G --> H[Risk classification]
H --> I[Form contract Operations]
Clasificación esperada
Ejemplo conceptual:
| Campo | Estado |
|---|---|
| Teléfono principal | Conocido / bajo riesgo |
| Teléfono secundario | Discovery necesario |
| Nombre / razón | Discovery necesario |
| Apellidos | Discovery necesario |
| Atención: propagación histórica detectada | |
| Dirección | Discovery necesario |
| Otros flags | Discovery necesario |
Evolución técnica
Hoy:
PATCH primary-phone
→ 1 field
→ 1 ETag
Posible siguiente modelo:
PATCH /customers/{id}/contact
→ ContactEditCommand
→ varios campos permitidos
→ 1 snapshot/version
→ 1 transacción
→ 1 audit coherente
No se decide todavía el contrato final hasta completar LPD.
23. Evolución hacia operaciones multisuministro
Las operaciones masivas serán una fase posterior.
Ejemplo de necesidad:
Cambiar cuenta bancaria
para varios suministros
No debe modelarse como:
for supply in supplies:
update(supply)
sin contrato de negocio.
Preguntas obligatorias:
- ¿qué entidad posee realmente la cuenta?;
- ¿se almacena en Contacto, Suministro o relación?;
- ¿qué side effects tiene Access?;
- ¿varios suministros deben compartir la misma referencia?;
- ¿cómo se valida previamente?;
- ¿cómo se autoriza?;
- ¿qué ocurre ante éxito parcial?;
- ¿cómo se reintenta?;
- ¿cómo se audita sin almacenar IBAN?;
- ¿cómo se revierte?;
- ¿qué confirmación humana se exige?
UX conceptual:
Operación: cambiar cuenta
↓
seleccionar suministros
↓
previsualizar alcance
↓
validar
↓
confirmación
↓
ejecutar operación
↓
resultado por lote
↓
audit
Aquí Operations dejará de ser simplemente un reemplazo de Access y podrá ofrecer una ergonomía operativa superior.
24. CLI y agentes IA
La CLI actual ixatu puede evolucionar, pero no debe convertirse en un atajo a SQL.
Modelo deseado:
flowchart LR
H[Humano React] --> API[Operations API]
A[Agent / CLI] --> API
API --> AUTH[Auth + authz]
AUTH --> UC[Use cases]
UC --> SQL[Writer]
SQL --> DB[(Azure SQL)]
Identidades:
Jesús
→ Entra Object ID humano
Izaskun
→ Entra Object ID humano
Automation Agent DEV
→ identidad técnica propia
Ventajas:
- mismo contrato;
- mismas validaciones;
- mismo ETag;
- mismo audit;
- atribución clara;
- sin credenciales SQL entregadas a agentes.
Posible línea futura:
ixatu contact ...
pero debe diseñarse como cliente de API.
25. Paso futuro a PROD
El paso a producción no es «activar una variable».
Debe ser una iniciativa explícita.
Gate futuro
DEV estable
→ patrón comprendido
→ acceptance suficiente
→ diseño PROD
→ principal writer PROD
→ grants mínimos
→ allowlist PROD
→ deployment controlado
→ smoke
→ human acceptance
→ pilot
→ observación
Convivencia
Durante el pilot:
Access sigue activo
+
Operations writer acotado
Cuando una capability esté suficientemente estabilizada:
desactivar función equivalente en Access
→ observar
→ estabilizar
→ continuar
Después, y no antes, puede plantearse el modelado/migración a PostgreSQL.
26. Reglas operativas y mnemotécnicas
Primero sustituir el comportamiento de Access; después sustituir su modelo de datos.
Un writer no recibe permisos para la tabla; recibe permisos para la capability.
Autenticación dice quién eres; autorización dice qué puedes hacer.
Entra no sustituye la allowlist.
ApplicationIntent=ReadOnly no sustituye los permisos SQL.
El ETag pertenece al snapshot de edición, no a la pantalla que lleva minutos abierta.
HTTP 412 no es un fallo: es una escritura peligrosa correctamente rechazada.
Audit registra la operación, no copia la PII.
DEV es donde aprendemos; PROD no es nuestro laboratorio.
Feature → CI → Director → merge → Deploy DEV → Human Acceptance.
No abstraer la incertidumbre.
El primer writer demuestra el carril; el siguiente demuestra que el carril es reusable.
Una operación multisuministro es una operación de negocio, no un bucle de UPDATE.
27. Riesgos y deuda conocida
27.1 Usuarios SQL personales amplios en DEV
Existen usuarios humanos con:
db_datareader
db_datawriter
e incluso un usuario técnico/externo con:
db_owner
No bloquea v0.9.0, pero merece auditoría posterior.
27.2 App Registration DEV
Durante Slice A se encontró una App Registration IXATU Operations DEV ya existente.
Su procedencia no quedó demostrada.
No bloqueó el writer, pero el inventario de identidades debería mantener trazabilidad de creación/ownership.
27.3 NPM advisory heredado
Durante Slice B, npm audit reportó una vulnerabilidad heredada en nanoid.
No fue introducida por el slice y las dependencias estaban congeladas.
Debe tratarse como deuda separada.
27.4 Release metadata todavía en 0.8.2
A fecha de este documento, el código funcional de writer está integrado, pero el bump de versión formal está diferido hasta cerrar el Release Gate.
Esto es intencionado.
27.5 PROD writer inexistente
No confundir:
PROD Web autenticada
con:
PROD write habilitado
Son cosas distintas.
28. Evidencia operativa (sin identificadores privados)
El Release Gate de v0.9.0 dejó evidencia de:
- slices A/B de foundation + UI;
- Deploy DEV sucesivos del backend writer, UI writer y ampliación de allowlist;
- escritura real en Azure SQL DEV;
- rechazo
412de stale write; - restauración exacta del valor de prueba;
- audit de éxito / conflicto / éxito;
- aceptación humana de UI.
Los Object IDs, IDs de contacto de prueba, URLs concretas de ACA y SHAs internos pertenecen al ledger operativo del repositorio IXATU Operations y no se publican aquí.
Conclusión
v0.9.0 representa un cambio de fase para IXATU Operations.
Hasta aquí, el proyecto había demostrado que podía consultar y presentar el legacy de manera moderna. El primer writer demuestra ahora que puede modificarlo sin renunciar a control, trazabilidad y seguridad.
La secuencia estratégica queda:
lectura legacy moderna
↓
primer writer controlado
↓
estabilización DEV
↓
formulario Contacto
↓
patrón reusable de writers
↓
automatización / CLI
↓
operaciones multisuministro
↓
pilot writer PROD
↓
sustitución progresiva de Access
El siguiente frente aprobado es Frente 2 — formulario Contacto, comenzando por LPD y diseño, no por programación inmediata.
La prioridad sigue siendo la misma:
hacer cada paso pequeño, observable, reversible y comprensible antes de aumentar el radio de escritura.