kcreate-scaffolding-gcp-script 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,623 @@
1
+ # Lecciones reales incorporadas en este skill
2
+
3
+ Cinco fuentes:
4
+
5
+ - **A — descubiertas desplegando `xoloshop-qa` a mano** (sesión previa,
6
+ documentadas también en `docs/CreateScriptsNewGCPProjectInstructions.md`
7
+ y `docs/DeployNewGCPProjectInstructions.md` de `club-tijuana/infra-cloud`).
8
+ - **B — descubiertas probando este skill de punta a punta** contra una
9
+ copia descartable de `infra-cloud` real, generando `xoloshop-prod` desde
10
+ `boletera-qa`, antes de considerarlo terminado. Varias de estas B
11
+ **nunca se habían notado** ni siquiera en el proceso manual (ej. los
12
+ archivos de nivel raíz, el dominio DNS) — la automatización las hizo
13
+ visibles por primera vez.
14
+ - **C — encontradas por `Review_V1.md`**, una revisión independiente que
15
+ también ejecutó el skill de punta a punta (contra `boletera-qa` y, de
16
+ forma crucial, contra `xoloshop-qa` — el entorno que las pruebas B no
17
+ habían usado como fuente). Ver `Review_V1.md` en la raíz de este repo
18
+ para el texto completo de la revisión y la respuesta punto por punto.
19
+ - **D — encontradas por `Review_V2.md`**, una re-revisión después de
20
+ aplicar C, con reintentos reales sobre entornos a medio construir
21
+ (encontró una regresión crítica del propio fix de reanudación de C1).
22
+ Ver `Review_V2.md`.
23
+ - **E — encontradas por `Review_V3.md`**, una tercera revisión que
24
+ reprodujo la cadena crítica de N-1 paso a paso y encontró que el
25
+ propio fix de `artifact_registry` (una función nueva de esa misma
26
+ ronda) se había quedado con la misma contaminación que D1/D2 ya habían
27
+ corregido en las demás. Ver `Review_V3.md`.
28
+
29
+ Cada lección dice **qué pasaba**, **por qué**, y **en qué script/función
30
+ de este skill queda resuelta**.
31
+
32
+ ---
33
+
34
+ ## A1 — Fórmula de CIDR sin validar produce octetos inválidos
35
+
36
+ **Qué pasaba:** extender a mano `10.N.0.0/22` sin revisar los 4 bloques
37
+ derivados produjo `10.335.0.0/17` (octeto `335`, inválido).
38
+ **Resuelto en:** `lib/common.sh:ksc_cidr_pick_free` — usa
39
+ `ipaddress.ip_network()` de Python (nunca aritmética a mano) y prueba
40
+ `N=1..249` hasta encontrar uno sin colisión; si no hay ninguno libre,
41
+ falla con un mensaje explícito en vez de devolver algo inválido.
42
+
43
+ ## A2 — Coma faltante editando `project_services` a mano
44
+
45
+ **Qué pasaba:** un elemento sin coma al final de la lista rompe
46
+ `terragrunt hcl validate` con un error de sintaxis poco claro.
47
+ **Resuelto en:** `build_component_project_services` — el archivo se copia
48
+ **completo, sin tocarlo**. Este skill nunca edita esa lista campo por
49
+ campo, así que esta clase de bug queda eliminada por diseño, no detectada
50
+ después.
51
+
52
+ ## A3 — Valores genéricos/org-wide en `artifact_registry`
53
+
54
+ **Qué pasaba:** `repository_id`/`description` quedaron como
55
+ `"club-tijuana-docker-repo"` / `"Official Club Tijuana docker
56
+ repository."` en un despliegue real, sin adaptar.
57
+ **Resuelto en:** `build_component_artifact_registry` — sustituye el
58
+ nombre de la app si lo encuentra en el valor. **Lección B encontrada
59
+ probando esto:** `boletera-qa`, la fuente real usada para probar este
60
+ skill, **todavía tiene ese mismo valor genérico sin corregir**. Como no
61
+ hay substring de la app para reemplazar, el script detecta el caso y
62
+ emite un `[WARN]` explícito en vez de copiar el problema en silencio —
63
+ nunca asume que "no encontré nada que sustituir" significa "está bien".
64
+
65
+ ## A4 — IP de `cloudadmin` transcrita mal
66
+
67
+ **Qué pasaba:** `177.226.233.64` en vez de `177.226.232.136` al copiar a
68
+ mano.
69
+ **Resuelto en:** `01_init_config.sh` extrae las entradas fijas (no
70
+ `dev-*`) de `authorized_networks` directo del `terragrunt.hcl` real de
71
+ `SOURCE_ENV` con `grep`/`sed`, nunca de memoria ni de una tabla.
72
+
73
+ ## A5 — IP de `dev-<usuario>` vieja o reusada
74
+
75
+ **Qué pasaba:** el ISP cambia; una IP de otra sesión queda obsoleta y
76
+ deja al usuario sin acceso.
77
+ **Resuelto en:** `lib/common.sh:ksc_fresh_public_ip`, llamada dentro de
78
+ `build_component_sql_postgres` — siempre `curl` en el momento de
79
+ construir, nunca cacheada entre corridas.
80
+
81
+ ## A6 — `gke_version`/`database_version` copiados a ciegas
82
+
83
+ **Qué pasaba:** no verificar si ya existe una versión estable más
84
+ reciente antes de copiar la de `SOURCE_ENV`.
85
+ **Resuelto en:** el config los autodetecta con el sufijo
86
+ `_NEEDS_VERIFICATION` en el nombre del campo — el skill los usa tal cual
87
+ estén en el config al construir, pero el nombre del campo mismo recuerda
88
+ que deben verificarse contra la documentación oficial antes de confiar en
89
+ ellos ciegamente (esto sí requiere criterio humano/del agente — no es
90
+ mecanizable sin consultar una fuente externa).
91
+
92
+ ## A7 — Falta `orgpolicy.googleapis.com` + sus 3 overrides
93
+
94
+ **Qué pasaba:** sin esta API y sus 3 overrides en `org_policies`, el
95
+ despliegue falla varios pasos después, con minutos de espera por
96
+ propagación cada vez que se corrige una política a la vez.
97
+ **Resuelto en:** `build_component_project_services` y
98
+ `build_component_org_policies` copian ambos archivos **completos** de
99
+ `SOURCE_ENV` — si `SOURCE_ENV` ya tiene la API y los 3 overrides (todo
100
+ entorno desplegado después de 2026-02-17 los tiene), `NEW_ENV` los hereda
101
+ automáticamente, sin que este skill necesite saber los nombres exactos de
102
+ las constraints.
103
+
104
+ ## A8 — `roles/orgpolicy.policyAdmin` en `project_iam`
105
+
106
+ **Qué pasaba:** GCP rechaza ese rol a nivel proyecto
107
+ (`Role ... is not supported for this resource`); solo existe a nivel
108
+ Organización/Carpeta.
109
+ **Resuelto en:** `build_component_project_iam` hace `grep` de ese string
110
+ después de copiar y **falla la construcción** (no solo advierte) si lo
111
+ encuentra — a diferencia de los demás `[WARN]`, este es un error real
112
+ conocido, no una zona gris.
113
+
114
+ ## A9 — Cambiar el casing de `project_name` sin que lo pidieran
115
+
116
+ **Qué pasaba:** confusión real entre `"XoloShop-QA"` y `"Xoloshop-QA"` en
117
+ el mismo PR.
118
+ **Resuelto en:** `lib/common.sh:ksc_infer_project_name` aplica una regla
119
+ fija y determinística (primera letra de la app en mayúscula, entorno
120
+ `QA`/`Prod`) y la deja en el config para que el humano la confirme en la
121
+ Etapa 1 — nunca se improvisa un casing distinto después de eso.
122
+
123
+ ## A10 — Bindings de `project_iam` específicos de otro equipo/app
124
+
125
+ **Qué pasaba:** copiar usuarios de un dominio externo o un rol
126
+ personalizado que no existe en el proyecto nuevo.
127
+ **Resuelto en:** `build_component_project_iam` copia el archivo completo
128
+ (no hay forma mecánica confiable de distinguir "binding específico de la
129
+ app" de "binding que debería estar en todo entorno" sin entender el
130
+ negocio) pero **siempre** emite un `[WARN]` pidiendo revisión humana
131
+ explícita antes de confiar en el archivo. Este es, a propósito, el punto
132
+ donde este skill deja más criterio al agente/humano — es el mismo punto
133
+ donde la guía manual original tampoco pudo mecanizarlo del todo.
134
+
135
+ ---
136
+
137
+ ## B1 — `live/<NEW_ENV>/` nunca se crea antes de copiar `project.hcl`
138
+
139
+ **Qué pasaba (encontrado probando):** la primera versión de
140
+ `10_build_components.sh` fallaba en el primer paso (`cp: cannot create
141
+ regular file ... No such file or directory`) porque nunca se hacía
142
+ `mkdir -p` del directorio del entorno nuevo antes de copiar `project.hcl`
143
+ dentro de él.
144
+ **Resuelto en:** `mkdir -p "$NEW_DIR"` explícito justo después de
145
+ descubrir los componentes, antes de cualquier copia.
146
+
147
+ ## B2 — Orden de construcción por alfabeto, no por dependencia real
148
+
149
+ **Qué pasaba (encontrado probando):** los componentes se descubren y
150
+ procesan alfabéticamente. `sql_private_access` necesita el rango CIDR que
151
+ calcula `vpc`, pero `"sql_private_access" < "vpc"` alfabéticamente, así
152
+ que se procesaba primero y fallaba por falta del archivo de CIDR.
153
+ **Resuelto en:** después de descubrir los componentes, si `vpc` está
154
+ presente se reordena al frente de la lista, antes que cualquier otro.
155
+
156
+ ## B3 — Variable de shell reusada como variable de loop
157
+
158
+ **Qué pasaba (encontrado probando):** dentro de `build_component_vpc`, la
159
+ variable local `f` (la ruta del `vpc/terragrunt.hcl` nuevo) se declaraba
160
+ una vez, pero más abajo un `for f in .../sql_private_access/terragrunt.hcl`
161
+ reusaba el mismo nombre como variable de loop — al terminar el loop, `f`
162
+ quedaba apuntando al **último archivo de otro entorno** recorrido, no al
163
+ `vpc/terragrunt.hcl` que se quería editar. Las siguientes llamadas
164
+ editaban el archivo equivocado (de otro entorno ya desplegado).
165
+ **Resuelto en:** la variable de loop se renombró a `sp_file`, y se agregó
166
+ `local f` explícito en `ksc_copy_component_files` como defensa adicional.
167
+ **Por qué importa documentarlo:** si agregas lógica nueva a cualquier
168
+ `build_component_*`, nunca reuses el nombre de una variable `local` ya
169
+ declarada en la misma función como variable de loop — bash no lo detecta
170
+ ni avisa, simplemente sobreescribe en silencio.
171
+
172
+ ## B4 — `sql_private_access.address` no lleva el prefijo CIDR
173
+
174
+ **Qué pasaba (encontrado probando):** el campo `address` de
175
+ `sql_private_access/terragrunt.hcl` es una IP **sin** sufijo `/NN` (el
176
+ prefijo va en un campo separado, `prefix_length`). El cálculo de rangos
177
+ nuevos producía strings `ipaddress.ip_network` completos
178
+ (`"10.0.0.0/16"`), y escribirlos tal cual en `address` genera un valor
179
+ inválido para ese módulo. Además, la recolección de rangos *existentes*
180
+ para evitar colisiones nunca leía estos `address`/`prefix_length`
181
+ separados — solo los CIDR completos de `vpc/terragrunt.hcl` — así que un
182
+ rango nuevo podía colisionar con un `sql_private_access` real sin que el
183
+ picker lo notara.
184
+ **Resuelto en:** `build_component_vpc` reconstruye el CIDR completo de
185
+ cada `sql_private_access/terragrunt.hcl` existente (`address` +
186
+ `prefix_length`) antes de buscar uno libre; `build_component_sql_private_access`
187
+ separa el rango elegido en `address` (sin sufijo) y `prefix_length` antes
188
+ de escribirlos.
189
+
190
+ ## B5 — Archivos de nivel raíz de `live/<entorno>/` (no de un componente)
191
+
192
+ **Qué pasaba (encontrado probando, con `20_validate_file_parity.sh`):**
193
+ `boletera-qa` tiene un `Makefile` (corre `terragrunt run --all ...`) y un
194
+ `README.md` directamente en `live/boletera-qa/`, fuera de cualquier
195
+ componente. Nada en el proceso — ni este skill en su primera versión, ni
196
+ el proceso manual que generó `xoloshop-qa` en la sesión anterior — los
197
+ copiaba. El propio `xoloshop-qa` real de `infra-cloud` sigue sin estos dos
198
+ archivos hoy.
199
+ **Resuelto en:** paso `top-level-files` dedicado en
200
+ `10_build_components.sh`, antes del loop de componentes.
201
+ **Por qué importa:** es la prueba de por qué existe la Etapa 3 — el
202
+ proceso manual original nunca detectó esta falta en su momento porque
203
+ nadie comparó cantidades de archivo a conciencia; el script de validación
204
+ de paridad sí.
205
+
206
+ ## B6 — Falso positivo en la validación de "project_id pinneado"
207
+
208
+ **Qué pasaba (encontrado probando):** la primera versión de
209
+ `21_validate_project_pinning.sh` exigía que **todo** componente tuviera
210
+ una clave `inputs.project_id = local.project_id`. Pero `org_policies` usa
211
+ `parent = "projects/${local.project_id}"` y
212
+ `storage_assets`/`storage_docs` usan `project = local.project_id` — nunca
213
+ una clave llamada `project_id` en `inputs`. El check marcaba estos 3
214
+ componentes como fallidos aunque estaban perfectamente bien pinneados.
215
+ **Resuelto en:** el check se generalizó a "¿aparece la substring exacta
216
+ `local.project_id` en algún lado del archivo?" — cubre cualquier forma en
217
+ que un módulo use el project id, sin asumir un nombre de clave fijo.
218
+
219
+ ## B7 — `dns_record_name`/`dns_domain_name` no son un patrón de texto genérico
220
+
221
+ **Qué pasaba (encontrado probando):** `sql_postgres` de varios entornos
222
+ (no `xoloshop-qa`, que nunca los configuró) tiene
223
+ `dns_record_name = "boletera.db"` y
224
+ `dns_domain_name = "qa.boletera.ninja."`. A simple vista parece el mismo
225
+ patrón `<app>.algo` que el resto de los campos, pero en realidad codifica
226
+ un **dominio DNS real que la organización tiene registrado para esa app
227
+ específica** (`boletera.ninja`, `cashless.ninja`) — no hay garantía de que
228
+ la app nueva vaya a vivir bajo `<NEW_ENV_APP>.ninja`.
229
+ **Resuelto en:** `build_component_sql_postgres` genera un valor por el
230
+ mismo patrón visual **solo si el campo ya existía en `SOURCE_ENV`**, pero
231
+ **siempre** lo marca con un `[WARN]` pidiendo confirmación humana — a
232
+ diferencia de los demás campos (nombres de red, SA, CIDR), donde la
233
+ sustitución mecánica sí es segura porque no representa una decisión de
234
+ negocio externa al repo.
235
+
236
+ ## B8 — `github_workload_identity` puede manejar varios repos de la misma app
237
+
238
+ **Qué pasaba (encontrado probando):** se asumió que este componente
239
+ siempre tiene exactamente 2 bindings (`terraform` + un `github-manager`).
240
+ `boletera-qa` real tiene **6**: el de `terraform` y 5 bindings
241
+ `github-manager` distintos, uno por cada repo de su propia arquitectura
242
+ multi-repo (`xbol-web-admin`, `xbol-api-client`, etc.). La primera versión
243
+ de este skill solo corregía el *segundo* binding encontrado y dejaba los
244
+ otros 4 con nombres de repo obsoletos de `SOURCE_ENV`, sin avisar.
245
+ **Resuelto en (versión original):** contar los bindings; si eran
246
+ exactamente 2 (caso simple) ajustar automáticamente, y si eran más, no
247
+ adivinar — solo `[WARN]`. **Superado por C6** (ver sección C): ahora el
248
+ bloque completo se reconstruye siempre, soportando una lista de repos en
249
+ `APP_GITHUB_REPO` — ya no hace falta elegir entre "caso simple" y
250
+ "advertir sin tocar".
251
+
252
+ ---
253
+
254
+ ## C1 — La construcción moría en el reintento (reanudación prometida pero rota)
255
+
256
+ **Qué pasaba:** `mkdir -p "$NEW_DIR"` corría antes que cualquier
257
+ componente, así que un fallo posterior dejaba `live/<NEW_ENV>/` creado —
258
+ y una guarda previa (`[[ ! -d "$NEW_DIR" ]] || ksc_die ...`) trataba eso
259
+ como "ya existe, no tocar" en el siguiente intento. La reanudación que
260
+ `SKILL.md`/`IDEA.md` prometían no existía en la práctica.
261
+ **Resuelto en:** se quitó la guarda. `10_build_components.sh` ahora
262
+ reconstruye cada componente siempre desde `SOURCE_DIR` (nunca incrementa
263
+ sobre `NEW_DIR`), así que el reintento es naturalmente idempotente — no
264
+ hace falta un motor de reanudación por paso, solo que nada dependa de un
265
+ estado parcial previo.
266
+
267
+ ## C2 — El picker de CIDR podía asignar un rango que se solapa consigo mismo
268
+
269
+ **Qué pasaba:** la fórmula probaba un solo `N` para los 4 bloques
270
+ (subred, pods, svc, `sql_private_access`) y solo los validaba contra los
271
+ rangos **ya existentes**, nunca entre sí. Cuando `N` resultaba múltiplo de
272
+ 10, la subred (`10.N.0.0/22`) quedaba **contenida** en su propio
273
+ `sql_private_access` (`10.N.0.0/16`) — un solapamiento real que solo se
274
+ detecta desplegando. Reproducido: el primer rango libre daba `N=1`
275
+ (válido), pero el segundo consecutivo daba `N=10` (inválido).
276
+ **Resuelto en:** `ksc_cidr_pick_free` ahora busca por década
277
+ (`sql_private_access = 10.<década>.0.0/16`) y, dentro de cada década, por
278
+ un offset ≥ 1 para la subred (preferido `+3`, la convención real
279
+ dominante del repo) — la subred nunca puede caer en la década exacta de
280
+ su propio `/16` por construcción, no solo por un chequeo adicional (que
281
+ también se agregó).
282
+
283
+ ## C3 — `boletera-qa` (el ejemplo del propio README) rompía el build
284
+
285
+ **Qué pasaba:** `build_top_level_files` fallaba la construcción completa
286
+ si `SOURCE_ENV` no tenía alguno de los 3 archivos de nivel raíz
287
+ (`Makefile`/`README.md`/`.gitignore`) — el caso real de `xoloshop-qa`,
288
+ que es justo el entorno más "sano" del repo (el único con
289
+ `orgpolicy.googleapis.com` y las 6 `org_policies`).
290
+ **Resuelto en:** copiar cada archivo de nivel raíz es ahora
291
+ condicional-sin-fallar (`if [[ -f ... ]]; then cp ...; fi`, nunca un `&&`
292
+ encadenado cuyo valor de verdad se propague como el de toda la función) —
293
+ que falte uno de los 3 es normal, no un error.
294
+
295
+ ## C4 — Project_iam/org_policies/project_services de `SOURCE_ENV` se copiaban sin verificar que estuvieran sanos
296
+
297
+ **Qué pasaba:** la estrategia "copiar tal cual" de A2/A7 asume
298
+ implícitamente que `SOURCE_ENV` ya tiene todo lo que un entorno nuevo
299
+ necesita. En la práctica, 4 de los 5 entornos reales de este repo NO
300
+ tienen `orgpolicy.googleapis.com` ni las 6 `org_policies` — construir
301
+ desde cualquiera de ellos (incluido `boletera-qa`, el ejemplo del README)
302
+ reproduce en silencio exactamente el error #7 de
303
+ `CreateScriptsNewGCPProjectInstructions.md`.
304
+ **Resuelto en:** `ksc_check_source_health` (nueva, en `lib/common.sh`,
305
+ invocada desde `02_print_config.sh` antes de confirmar) compara
306
+ `SOURCE_ENV` contra el resto de `live/` y advierte si le falta una API o
307
+ le faltan entradas de `org_policies` que **el entorno modificado más
308
+ recientemente** ya tiene. Se usa "el más reciente" y no "la mayoría"
309
+ porque un requisito nuevo de la organización, por definición, todavía NO
310
+ lo tiene la mayoría de entornos viejos — una votación por mayoría nunca
311
+ lo habría detectado (se probó: con mayoría, el hueco real de
312
+ `boletera-qa` no se veía; con "más reciente" sí). La misma función
313
+ también advierte si la IP fija de `cloudadmin` de `SOURCE_ENV` difiere de
314
+ la mayoría del resto de `live/` (ahí sí conviene mayoría, no "más
315
+ reciente" — una IP de oficina compartida rara vez cambia legítimamente).
316
+ Nunca bloquea nada — solo advierte, porque un `SOURCE_ENV` minimalista a
317
+ propósito es una posibilidad legítima que el humano puede confirmar.
318
+
319
+ ## C5 — Sustitución incompleta del nombre de la app fuera de `artifact_registry`
320
+
321
+ **Qué pasaba:** solo se sustituía el token completo del entorno
322
+ (`boletera-qa` → `shopdemo-qa`), nunca el nombre corto de la app
323
+ (`boletera`) ni su capitalizado (`Boletera`), salvo en
324
+ `artifact_registry`. Resultado real: `service_accounts` dejaba
325
+ `display_name = "Boletera QA Node Service Account"` y `project_iam`
326
+ dejaba namespaces/bindings con `boletera` sin tocar.
327
+ **Resuelto en:** `ksc_copy_component_files` (la función que TODO
328
+ `build_component_*` usa para copiar) ahora aplica, para cualquier
329
+ componente, tres sustituciones en orden: el token completo del entorno,
330
+ el nombre corto de la app en minúscula, y el nombre corto capitalizado —
331
+ una sola vez, en un solo lugar, en vez de que cada componente decidiera
332
+ por su cuenta cuáles aplicar (y se le olvidara a varios).
333
+
334
+ ## C6 — Editar `github_workload_identity` por posición era frágil, y no soportaba apps multi-repo
335
+
336
+ **Qué pasaba:** el binding de `github-manager` se identificaba por ser
337
+ la "2.ª coincidencia" de `principal_set_suffix` en el archivo — si
338
+ `SOURCE_ENV` tuviera los 2 bindings en el orden inverso, se habría
339
+ corrompido el binding de `terraform`/`infra-cloud` en vez del de la app.
340
+ Además, cuando `SOURCE_ENV` tenía más de 2 bindings (apps multi-repo como
341
+ `boletera-qa`, con 5), el skill solo advertía y dejaba los 5 nombres de
342
+ repo viejos sin tocar.
343
+ **Resuelto en:** `ksc_set_workload_identity_bindings` reconstruye el
344
+ bloque completo: ubica el binding de `terraform` **por contenido**
345
+ (`service_account_email` que empieza con `"terraform@"`, nunca por
346
+ posición) y genera un binding `github-manager` por cada repo listado en
347
+ `APP_GITHUB_REPO` (separados por `;`) — soporta tanto el caso simple (1
348
+ repo) como el multi-repo, sin advertir-y-no-tocar.
349
+
350
+ ## C6b — No todo entorno real tiene un binding de terraform en `github_workload_identity`
351
+
352
+ **Qué pasaba (encontrado verificando el fix de C6 con una CUARTA fuente,
353
+ `cashless-qa`, que ninguna ronda anterior de pruebas había usado):**
354
+ `cashless-qa` real solo tiene un binding `github-manager` — nunca tuvo
355
+ uno de `terraform`. `ksc_set_workload_identity_bindings` (y la extracción
356
+ previa en `build_component_github_workload_identity`) asumían que ese
357
+ binding siempre existe y fallaban la construcción completa si no lo
358
+ encontraban.
359
+ **Resuelto en:** si no hay binding de `terraform@...`, no es un error —
360
+ se usa `"infra-cloud"` como `principal_set_suffix` por defecto (el valor
361
+ que la guía §3.15 pide siempre) y se avisa con `[WARN]`, en vez de morir.
362
+
363
+ ## C7 — Lecciones menores de la misma revisión
364
+
365
+ - **Componentes opcionales copiados siempre:** el skill copiaba *todos*
366
+ los componentes que `SOURCE_ENV` tuviera, incluyendo los que la guía
367
+ (§1.2) pide explícitamente no incluir por defecto
368
+ (`cloud_armor`/`identity_platform`/`monitoring_alerts`/`secrets`).
369
+ Resuelto con `ksc_base_components` (intersección real de componentes en
370
+ TODOS los entornos de `live/`, sin lista hardcodeada — y excluyendo
371
+ explícitamente cualquier entorno que no siga el patrón `<app>-qa|prod>`,
372
+ como un proyecto de infraestructura compartida) + el campo
373
+ `EXTRA_COMPONENTS` del config para pedirlos de vuelta explícitamente.
374
+ - **Tracking como dependencia dura:** si `github-proyect-tracking` no
375
+ podía inicializar (sin red, token, URL inválida), toda la construcción
376
+ moría — aunque es 100% local. Resuelto: el tracking es ahora
377
+ best-effort (`[WARN]` + seguir sin reanudación remota).
378
+ - **`deletion_protection` en `*-prod`:** se copiaba de `SOURCE_ENV` sin
379
+ aviso; si `SOURCE_ENV` es qa (`"false"`) y `NEW_ENV` es prod, quedaba
380
+ `"false"` en prod en silencio. Resuelto con un `[WARN]` específico.
381
+ - **Clave HCL duplicada heredada de `SOURCE_ENV`** (ej.
382
+ `boletera-qa/sql_postgres` trae `update_timeout` dos veces dentro del
383
+ mismo bloque `inputs`): ni `terragrunt hcl fmt` ni `hcl validate` lo
384
+ detectan. Resuelto con `ksc_check_duplicate_keys`, corrido al final de
385
+ la construcción sobre cada componente (cuidado: tiene que ser
386
+ consciente de bloques `{ }` — una primera versión confundía
387
+ `locals.project_id` con `inputs.project_id` como "duplicado" por tener
388
+ la misma indentación).
389
+ - **Dos falsos positivos de los propios chequeos de este skill, encontrados
390
+ recién al probar con `xoloshop-qa` como fuente** (no con `boletera-qa`):
391
+ el comentario que el propio skill agrega en `project_iam` explicando por
392
+ qué no se puede usar `roles/orgpolicy.policyAdmin` ahí contiene ese
393
+ mismo string textualmente, y el chequeo de "¿quedó algo de SOURCE_ENV
394
+ sin sustituir?" no distinguía comentarios de código real. Ambos
395
+ chequeos ahora ignoran líneas que empiezan con `#`.
396
+ - **`terragrunt hcl validate` por componente** (guía §3.17) no se corría,
397
+ solo `hcl fmt`. Se agregó al final de la construcción, tolerando el
398
+ falso positivo conocido de `gke_private`.
399
+ - Bug de texto: un carácter de guion suave invisible (U+00AD) dentro de
400
+ "realmente" en un mensaje `[WARN]`, y la plantilla del config decía
401
+ "separadas por coma" cuando el formato real usa `;`. Ambos corregidos.
402
+
403
+ ---
404
+
405
+ ## D — Encontradas por `Review_V2.md` (re-revisión después de aplicar C)
406
+
407
+ `Review_V2.md` volvió a ejecutar el skill de punta a punta contra las
408
+ correcciones de la sección C, con varias fuentes y, de forma crítica,
409
+ con reintentos reales sobre entornos a medio construir. Encontró un bug
410
+ crítico nuevo — una regresión directa de C1 — y varios menores.
411
+
412
+ ## D1 (N-1) — CRÍTICO: la base de componentes se contaminaba con entornos sin comitear
413
+
414
+ **Qué pasaba:** `ksc_base_components` contaba cualquier directorio de
415
+ `live/` que pareciera un entorno (`^[a-z0-9]+-(qa|prod)$`) — incluido un
416
+ `live/<NEW_ENV>/` a medio construir (el estado normal de un reintento
417
+ desde C1) y, peor, cualquier OTRO entorno abandonado de una corrida
418
+ anterior, con cualquier nombre. Reproducido en cadena: un entorno
419
+ mutilado a 2 componentes hacía que un entorno nuevo e inocente **también**
420
+ construyera solo 2 componentes, con `FILE_PARITY_OK`/`PROJECT_PINNING_OK`
421
+ en verde — exactamente el defecto silencioso que este skill existe para
422
+ eliminar.
423
+ **Resuelto en:** `ksc_real_envs` (nueva, consolidada): un entorno solo
424
+ cuenta si además de cumplir el patrón de nombre tiene **al menos un
425
+ archivo comiteado en git** bajo su `live/<env>/`. Este skill nunca
426
+ comitea nada, así que cualquier intento (parcial o completo, propio o de
427
+ otro `NEW_ENV`) queda excluido automáticamente, sin tener que conocer su
428
+ nombre ni asumir que "bien formado" significa "completo". Se usa en
429
+ `ksc_base_components`, `ksc_check_source_health` y
430
+ `ksc_majority_fixed_network` — las tres tenían el mismo filtro
431
+ incompleto (D2).
432
+
433
+ ## D2 — El mismo filtro incompleto estaba duplicado en 3 funciones
434
+
435
+ **Qué pasaba (encontrado aplicando el fix de D1):** el filtro
436
+ "solo entornos reales" estaba copiado a mano en `ksc_base_components`,
437
+ `ksc_check_source_health` y `ksc_majority_fixed_network` por separado.
438
+ Corregirlo solo en la primera (como un primer intento de fix hizo)
439
+ dejaba las otras dos igual de contaminables — confirmado probando: con
440
+ dos entornos de prueba sin comitear en `live/`, el doctor reportaba "6/7
441
+ entornos reales" en vez de "4/5".
442
+ **Resuelto en:** las tres ahora llaman a `ksc_real_envs`, una sola
443
+ fuente de verdad. Lección operativa: cuando un fix corrige una función,
444
+ `grep` el resto del archivo por lógica duplicada antes de dar el fix por
445
+ completo.
446
+
447
+ ## D3 — Bug de `set -e` introducido por el propio fix de N-6
448
+
449
+ **Qué pasaba (encontrado probando el fix, antes de llegar a reportarlo):**
450
+ el código nuevo para preferir la IP mayoritaria (ver D5) usaba
451
+ `[[ condición ]] && variable=valor` como declaración suelta dentro de un
452
+ `for`, en un script con `set -e`. Cuando la condición daba falso para
453
+ **cualquier** entorno (ej. `live/shared/`, que no matchea
454
+ `<app>-qa|prod>`), el script completo moría ahí mismo — y como venía
455
+ ANTES del bloque que autodetecta `ORG_ID`/`GKE_VERSION`/etc., esos
456
+ campos quedaban `__PENDIENTE__` sin ninguna pista de por qué.
457
+ **Resuelto:** `if`/`fi` en vez de `&&` para cualquier asignación con
458
+ efecto secundario que dependa de una condición que puede ser falsa,
459
+ dentro de un script con `set -e`. Lección operativa: `A && B` es seguro
460
+ bajo `set -e` si B puede fallar (A fallar está exento), pero mezclar
461
+ asignaciones de esta forma invita a confundirse — mejor `if` siempre que
462
+ el cuerpo no sea un one-liner trivial como `continue`.
463
+
464
+ ## D4 (N-2) — `01_init_config.sh` moría en un rescate legítimo
465
+
466
+ **Qué pasaba:** después del fix de reanudación de C1, `10` ya no
467
+ fallaba por que `live/<NEW_ENV>/` existiera — pero `01` sí, con un
468
+ mensaje que literalmente decía "bórralo tú mismo primero", contradiciendo
469
+ la instrucción de `SKILL.md` de simplemente re-correr `10`.
470
+ **Resuelto en:** `01` ahora distingue "`live/<NEW_ENV>/` existe y el
471
+ config de este skill YA existe" (reintento legítimo, no es error) de
472
+ "existe pero no hay config" (colisión real con otra cosa, sí es error).
473
+
474
+ ## D5 (N-6) — El doctor avisaba del typo pero el config igual escribía el valor malo
475
+
476
+ **Qué pasaba:** `ksc_check_source_health` ya sabía que la mayoría de
477
+ `live/` usa `177.226.232.136/32` para `cloudadmin` y que `xoloshop-qa`
478
+ tiene un valor distinto — pero solo avisaba; `01_init_config.sh` seguía
479
+ autodetectando y escribiendo el valor de `SOURCE_ENV` tal cual.
480
+ **Resuelto en:** `ksc_majority_fixed_network` (extraída del doctor,
481
+ reusada) + lógica en `01_init_config.sh` que adopta el valor mayoritario
482
+ en el config cuando hay una mayoría clara (>mitad de los entornos
483
+ reales) y difiere del de `SOURCE_ENV`, con un `[WARN]` explicando el
484
+ cambio (no en silencio).
485
+
486
+ ## D6 (N-7) — El picker de CIDR preferiría la década 0 o rellenaría huecos bajos
487
+
488
+ **Qué pasaba:** el picker probaba décadas en orden `0, 10, 20, ...` —
489
+ técnicamente sin colisión, pero `10.0.x.x` se reserva convencionalmente
490
+ (VPN/defaults/on-prem) y rellenar huecos bajos (`10, 50, ...`) en vez de
491
+ seguir la secuencia real (`20, 30, 40, 130, 140`) es menos predecible
492
+ para quien revisa el PR.
493
+ **Resuelto en:** el orden de búsqueda ahora prioriza la primera década
494
+ libre **por encima** de la mayor ya usada, y la década 0 queda como
495
+ último recurso, no primera opción.
496
+
497
+ **Corrección posterior (Review V3, P-4):** la primera versión de esta
498
+ lección decía "el siguiente lógico es 150" — eso es matemáticamente
499
+ imposible con la fórmula real: `svc = 10.<década+off+106>.0.0/23`
500
+ necesita `década+off ≤ 149` para que el segundo octeto no pase de 255,
501
+ lo que limita la década máxima usable a **140** sin importar el offset.
502
+ El código ya se comportaba bien (`candidate()` devuelve `None` para
503
+ cualquier década inválida y el loop sigue), pero probaba décadas hasta
504
+ 240 sin necesidad; se acotó el rango generado a `0..140` para que el
505
+ código y el comentario coincidan con el techo real de la fórmula.
506
+
507
+ ## D7 — Mejora pendiente resuelta: `artifact_registry` determinista sin hardcodear la organización
508
+
509
+ Encontrada como mejora abierta en la propia `Review_V2.md` (no un bug):
510
+ cuando `SOURCE_ENV` tiene `repository_id`/`description` genéricos (4 de
511
+ 5 entornos reales), en vez de solo advertir, `ksc_learn_artifact_registry_template`
512
+ busca OTRO entorno de `live/` que sí tenga su propio nombre de app en su
513
+ `artifact_registry`, extrae el patrón reemplazando ese nombre por un
514
+ marcador, y lo aplica con el nombre de la app nueva. Nunca se hardcodea
515
+ "club-tijuana" ni ningún otro literal de la organización en el código
516
+ del skill — se aprende de los datos reales de `live/`, igual que el
517
+ doctor.
518
+
519
+ ## D8 — Lecciones menores de la misma re-revisión
520
+
521
+ - **Cobertura del leftover-check reducida (N-3):** V1 revisaba todos los
522
+ archivos de `live/<NEW_ENV>/`; el fix de ignorar comentarios (B-algo)
523
+ de la ronda anterior redujo por accidente la lista a solo
524
+ `terragrunt.hcl`/`project.hcl`/`README.md`, perdiendo
525
+ `Makefile`/`.gitignore` de cada componente. Restaurado con `find` sobre
526
+ todo el árbol (2 niveles), filtrando comentarios línea por línea en vez
527
+ de limitar por nombre de archivo.
528
+ - **WARNs del doctor no sobrevivían a la Etapa 1 (N-5):** `KSC_WARNINGS_FILE`
529
+ solo se fijaba en `10_build_components.sh`; si la sesión moría entre
530
+ `02` y `10`, los avisos del doctor se perdían. Ahora `02` también la fija
531
+ (truncando, fresca cada corrida) y `10` hace *append* en vez de truncar.
532
+ - **Código muerto (N-4):** una variable (`DUPLICATE_ISSUES`) que se
533
+ asignaba y nunca se leía. Eliminada.
534
+ - **`SKILL.md` no cubría `finalize` sin tracking (N-8):** si el tracking
535
+ nunca se inicializó, la instrucción de cerrar con `run_tracking.sh
536
+ finalize` fallaría sin explicación. Se agregó la condición explícita.
537
+ - **Cosmético (N-9):** `uninstall.sh --clean-gitignore` dejaba una línea
538
+ en blanco suelta al final del `.gitignore`.
539
+
540
+ ---
541
+
542
+ ## E1 (P-1) — La función nueva de D7 nació con la misma contaminación que D1/D2 ya habían corregido
543
+
544
+ **Qué pasaba:** `ksc_learn_artifact_registry_template` (agregada en la
545
+ misma ronda que D7) iteraba `live/*/` directamente, sin pasar por
546
+ `ksc_real_envs` — exactamente el filtro incompleto que D1/D2 ya habían
547
+ consolidado en las otras tres funciones. Reproducido: un entorno sin
548
+ comitear (`live/aaaajunk-qa/`) con `repository_id =
549
+ "aaaajunk-mega-custom-repo"` se "aprendía" como si fuera una plantilla
550
+ real, con un `[OK]` que además afirmaba (incorrectamente) que era un
551
+ "entorno real".
552
+ **Resuelto en:** la función ahora usa `ksc_real_envs` como las demás, y
553
+ de paso adoptó el mismo criterio de "preferir el más reciente" del
554
+ doctor en vez de "el primero alfabético" — dos entornos específicos con
555
+ nombres distintos ya no dependen de en qué orden los liste el
556
+ filesystem.
557
+ **Lección operativa, otra vez:** cada función nueva que itere `live/*/`
558
+ tiene que pasar por `ksc_real_envs` desde el día uno — no basta con
559
+ haberlo corregido en las funciones que ya existían (D2 ya dejó esta
560
+ lección una vez; aquí se repitió con una función que no existía
561
+ todavía cuando D2 se escribió).
562
+
563
+ ## E2 (P-2) — Rescate imposible si se perdía el config
564
+
565
+ **Qué pasaba:** si `.kcreate-scaffolding/<env>.config` se perdía (ej.
566
+ por `uninstall.sh --clean-gitignore`, que la borra a propósito) con
567
+ `live/<NEW_ENV>/` todavía a medias, `01_init_config.sh` trataba cualquier
568
+ `live/<NEW_ENV>/` sin config como colisión real y recomendaba borrar
569
+ exactamente lo que se quería rescatar — sin camino automático de vuelta.
570
+ **Resuelto en:** `01` ahora reconoce una firma mínima
571
+ (`project.hcl` con `project_id` == `NEW_ENV`, que `build_project_hcl`
572
+ siempre deja) para distinguir "rescate legítimo" de "colisión real", y
573
+ regenera el config desde cero en el primer caso.
574
+
575
+ ## E3 (P-3) — Los warnings de la Construcción se duplicaban en cada reintento
576
+
577
+ **Qué pasaba:** el fix de persistencia de D-faltante-17/N-5 usó un solo
578
+ archivo en modo *append* para doctor + build. Correcto para que el
579
+ doctor sobreviva entre `02` y `10`, pero cada reintento de `10` volvía a
580
+ escribir sus propios WARN sobre los de la corrida anterior — un archivo
581
+ que pasaba de 8 a 14 líneas con un solo retry, sin límite.
582
+ **Resuelto en:** dos archivos separados
583
+ (`ksc_warnings_file_path` para el doctor, `ksc_build_warnings_file_path`
584
+ para la construcción). El del doctor se trunca en cada `02` (sigue
585
+ fresco); el de la construcción se trunca al INICIO de cada `10` (no
586
+ repite sus propios WARN en un retry). El reporte final combina ambos.
587
+
588
+ ## E4 (P-4) — Comentario/lección describía un comportamiento matemáticamente imposible
589
+
590
+ **Qué pasaba:** D6 (y el comentario del código) decían que el picker de
591
+ CIDR, al no encontrar una década libre cerca, seguiría hasta "150, 160,
592
+ ...". La fórmula real (`svc = 10.<década+off+106>.0.0/23`) hace que
593
+ cualquier década ≥ 150 exceda 255 en el segundo octeto para todo
594
+ offset — esas décadas nunca se usan, sin importar qué tan lejos llegue
595
+ el rango de búsqueda. El código ya se comportaba bien (`candidate()`
596
+ devuelve `None` y el loop sigue), solo el comentario/lección prometían
597
+ algo que no podía pasar.
598
+ **Resuelto en:** el rango de búsqueda ahora se acota a `0..140` (el
599
+ techo real de la fórmula) y el comentario/D6 se corrigieron para
600
+ describir el comportamiento real, no uno aspiracional.
601
+
602
+ ## E5 — Observación: repo destino sin ningún commit
603
+
604
+ Si `live/` no tiene NINGÚN entorno comiteado (un repo de infra recién
605
+ creado, sin commit inicial), `ksc_real_envs` da una lista vacía y la
606
+ base de componentes también queda vacía — el build excluiría todo salvo
607
+ lo que `EXTRA_COMPONENTS` pida. No ocurre en un repo de infra real (que
608
+ siempre tiene historia), pero se agregó un `[WARN]` explícito en
609
+ `ksc_base_components` para ese caso, en vez de dejar que "todo se
610
+ excluye" pase sin ninguna pista de la causa.
611
+
612
+ ---
613
+
614
+ ## Qué significa esta lista para quien edite este skill después
615
+
616
+ Cada vez que se agregue un `build_component_<algo>` nuevo o se modifique
617
+ uno existente: **pruébalo contra una copia descartable de un repo real**
618
+ (nunca contra el repo de trabajo del usuario) antes de considerarlo
619
+ terminado, igual que se hizo para construir las lecciones B1-B8. La mitad
620
+ de los bugs reales de este skill nunca se habrían notado solo leyendo el
621
+ código — se encontraron ejecutándolo de punta a punta contra datos reales
622
+ y revisando el resultado campo por campo, no solo confiando en que los
623
+ checks automatizados pasaran.