data_drain 0.1.19 → 0.2.1
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +4 -0
- data/README.md +66 -171
- data/docs/IMPROVEMENT_PLAN.md +1162 -0
- data/docs/execution/archive/v0.2.0.agente-review.md +125 -0
- data/docs/execution/archive/v0.2.0.md +812 -0
- data/docs/glue_pyspark_example.py +60 -0
- data/lib/data_drain/engine.rb +53 -40
- data/lib/data_drain/file_ingestor.rb +40 -25
- data/lib/data_drain/record.rb +24 -3
- data/lib/data_drain/storage/s3.rb +48 -6
- data/lib/data_drain/validations.rb +17 -0
- data/lib/data_drain/version.rb +1 -1
- data/lib/data_drain.rb +2 -0
- data/skill/SKILL.md +215 -0
- data/skill/references/antipatrones.md +242 -0
- data/skill/references/api-detallada.md +257 -0
- data/skill/references/eventos-telemetria.md +154 -0
- metadata +11 -2
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
# Plan de Ejecución — v0.2.0
|
|
2
|
+
|
|
3
|
+
**Release objetivo:** v0.2.0 — Hardening de seguridad y testing
|
|
4
|
+
**Items del roadmap:** 1, 2, 3, 4 ([ver IMPROVEMENT_PLAN.md](../IMPROVEMENT_PLAN.md#p0--seguridad-y-correctitud-v020))
|
|
5
|
+
**Branch:** `feature/v0.2.0` → mergeado a `main` (commit `e1d121b`)
|
|
6
|
+
**Estado:** ✅ Completado — v0.2.0 release el 2026-04-13
|
|
7
|
+
**Última actualización:** 2026-04-13
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Resumen
|
|
12
|
+
|
|
13
|
+
Este release cierra los gaps P0 de DataDrain: SQL injection en `table_name`, credenciales S3 expuestas, memory leak de conexión DuckDB, y la cobertura de tests insuficiente. Resultado esperado: gema producción-ready para datos sensibles con > 80% line coverage.
|
|
14
|
+
|
|
15
|
+
**Items del release:**
|
|
16
|
+
|
|
17
|
+
| Item | Resumen | Estimación |
|
|
18
|
+
|------|---------|-----------|
|
|
19
|
+
| 2 | Validación regex de `table_name`/`primary_key` | 1-2h |
|
|
20
|
+
| 1 | Migrar credenciales S3 a `credential_chain` | 2-4h |
|
|
21
|
+
| 3 | Cleanup conexión DuckDB thread-local (`Record.disconnect!`) | 4-6h |
|
|
22
|
+
| 4 | Cobertura tests P0 (Record, Storage, GlueRunner, Observability, Configuration) | 1-2 días |
|
|
23
|
+
|
|
24
|
+
**Total estimado:** 2-3 días de trabajo enfocado.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Review de agentes — incorporado
|
|
29
|
+
|
|
30
|
+
Este plan fue revisado por **opencode/big-pickle** (`v0.2.0.agente-review.md`, 2026-04-13). Los 4 issues + 2 preguntas planteados fueron incorporados:
|
|
31
|
+
|
|
32
|
+
| Issue / Q | Resolución | Ubicación en este plan |
|
|
33
|
+
|-----------|-----------|----------------------|
|
|
34
|
+
| Issue 1: `aws_region` también necesita escape | `safe_region = escape_sql(region)` antes de heredoc | Fase 3.2 |
|
|
35
|
+
| Issue 2: convención `:integration` tag | Definida con `define_derived_metadata` | Fase 0.4 |
|
|
36
|
+
| Issue 3: aws-sdk-s3 versión | Verificación añadida a Fase 0.1 | Fase 0.1 |
|
|
37
|
+
| Issue 4: orden ParquetFixtures vs spec_helper | Trigger en spec_helper, módulo en support | Fase 5.2 |
|
|
38
|
+
| Q1: caracteres a escapar en DuckDB | Verificación manual en `bin/console` antes de Fase 1 | Fase 0.1 |
|
|
39
|
+
| Q2: callers con `public.table` | `rg` en monorepo Wispro antes de Fase 1 | Fase 0.1 |
|
|
40
|
+
| R1: comentario en spec_helper sobre cleanup | Aplicado a `config.after(:each)` | Fase 0.5 |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Orden de ejecución y dependencias
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Fase 0 (setup)
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
Fase 1: Item 2 (validación regex) ──────► sin deps, calienta
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
Fase 2: Setup tests baseline (Configuration, Observability, JsonType, Storage factory)
|
|
54
|
+
│
|
|
55
|
+
├──► Fase 3: Item 1 (S3 secret) + tests Storage::S3
|
|
56
|
+
│ │
|
|
57
|
+
│ ▼
|
|
58
|
+
├──► Fase 4: Tests Storage::Local + GlueRunner
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
Fase 5: Item 3 (disconnect!) + tests Record
|
|
62
|
+
│
|
|
63
|
+
▼
|
|
64
|
+
Fase 6: Tests Engine + FileIngestor (refuerzo + nuevos casos)
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
Fase 7: Lint + Coverage report + CHANGELOG + version bump + commit final
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Razonamiento del orden:**
|
|
71
|
+
- Item 2 primero: cheap, sin deps, establece patrón de validación + tests.
|
|
72
|
+
- Tests baseline antes de los items grandes: te dan una red para no romper.
|
|
73
|
+
- Item 1 después de tener `storage/s3_spec.rb` esqueleto.
|
|
74
|
+
- Item 3 último porque requiere tests de Record ya escritos para validar idempotencia.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Pre-requisitos (Fase 0)
|
|
79
|
+
|
|
80
|
+
### 0.1 Verificar entorno
|
|
81
|
+
|
|
82
|
+
- [ ] `bundle install` corre limpio
|
|
83
|
+
- [ ] `bundle exec rspec` actual pasa (4 tests verdes)
|
|
84
|
+
- [ ] `bundle exec rubocop` actual sin ofensas
|
|
85
|
+
- [ ] DuckDB version en `Gemfile.lock` ≥ 1.4 (para `CREATE SECRET`)
|
|
86
|
+
- [ ] **aws-sdk-s3 ≥ 1.114** (para `Aws::S3::Client.new(stub_responses: true)`):
|
|
87
|
+
```bash
|
|
88
|
+
bundle exec ruby -e "require 'bundler'; puts Bundler.load.specs.find { |s| s.name == 'aws-sdk-s3' }.version"
|
|
89
|
+
```
|
|
90
|
+
Si < 1.114 → bumpear `Gemfile`.
|
|
91
|
+
- [ ] **Q1 resuelta — caracteres a escapar en DuckDB SQL strings.** Probar en `bin/console`:
|
|
92
|
+
```ruby
|
|
93
|
+
conn.query("CREATE OR REPLACE SECRET t (TYPE S3, REGION 'us-east-1''); SELECT 1;--')")
|
|
94
|
+
# Confirmar si SQL estándar (solo `'` → `''`) es suficiente o si DuckDB tiene reglas extra
|
|
95
|
+
```
|
|
96
|
+
Documentar resultado acá:
|
|
97
|
+
> Resultado: ___________________
|
|
98
|
+
- [ ] **Q2 resuelta — buscar callers con `public.table` o schema en monorepo Wispro:**
|
|
99
|
+
```bash
|
|
100
|
+
rg "table_name.*[\"']\w+\.\w+" --type ruby
|
|
101
|
+
rg "DataDrain::Engine.new" -A 5 --type ruby
|
|
102
|
+
```
|
|
103
|
+
Si hay matches con schema explícito → coordinar con Item 2 (relajar regex o forzar split de schema).
|
|
104
|
+
Documentar:
|
|
105
|
+
> Callers encontrados con schema: ___________________
|
|
106
|
+
|
|
107
|
+
### 0.2 Crear branch
|
|
108
|
+
|
|
109
|
+
- [ ] `git checkout -b feature/v0.2.0`
|
|
110
|
+
|
|
111
|
+
### 0.3 Agregar SimpleCov
|
|
112
|
+
|
|
113
|
+
- [ ] Agregar a `Gemfile`:
|
|
114
|
+
```ruby
|
|
115
|
+
group :test do
|
|
116
|
+
gem "simplecov", require: false
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
- [ ] Agregar al inicio de `spec/spec_helper.rb`:
|
|
120
|
+
```ruby
|
|
121
|
+
require "simplecov"
|
|
122
|
+
SimpleCov.start do
|
|
123
|
+
add_filter "/spec/"
|
|
124
|
+
minimum_coverage 80 # falla si baja del 80%
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
- [ ] `bundle install`
|
|
128
|
+
- [ ] `bundle exec rspec` y verificar baseline de cobertura actual reportada en `coverage/index.html`
|
|
129
|
+
- [ ] Agregar `coverage/` a `.gitignore` si no está
|
|
130
|
+
- [ ] Commit: `chore: add simplecov for coverage tracking`
|
|
131
|
+
|
|
132
|
+
### 0.4 Estructura de fixtures
|
|
133
|
+
|
|
134
|
+
- [ ] Crear `spec/fixtures/`
|
|
135
|
+
- [ ] Crear `spec/fixtures/sample.csv` con 5 filas dummy:
|
|
136
|
+
```csv
|
|
137
|
+
id,timestamp,isp_id,value
|
|
138
|
+
1,2026-01-01 10:00:00,42,100
|
|
139
|
+
2,2026-01-02 11:00:00,42,200
|
|
140
|
+
3,2026-02-01 12:00:00,43,150
|
|
141
|
+
4,2026-02-15 13:00:00,42,175
|
|
142
|
+
5,2026-03-01 14:00:00,43,225
|
|
143
|
+
```
|
|
144
|
+
- [ ] Decidir si tests integration con Postgres son parte de este release (recomendación: marcar como `:integration` y skip por default en CI; correr a mano)
|
|
145
|
+
- [ ] **Definir convención `:integration` tag** en `spec/spec_helper.rb`:
|
|
146
|
+
```ruby
|
|
147
|
+
RSpec.configure do |config|
|
|
148
|
+
config.define_derived_metadata(:integration) do |metadata|
|
|
149
|
+
metadata[:skip] = "Integration test — requiere Postgres real (correr con --tag integration)"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
Tests integration se marcan con `it "...", :integration do`. Para correrlos: `bundle exec rspec --tag integration` (requiere remover/sobrescribir el skip; alternativa: usar `metadata[:skip] = !ENV["RUN_INTEGRATION"]` para activar con env var).
|
|
154
|
+
- [ ] Commit: `chore: add test fixtures structure`
|
|
155
|
+
|
|
156
|
+
### 0.5 Helper de specs
|
|
157
|
+
|
|
158
|
+
- [ ] Crear `spec/support/` y agregar requires en `spec_helper.rb`:
|
|
159
|
+
```ruby
|
|
160
|
+
Dir[File.expand_path("support/**/*.rb", __dir__)].sort.each { |f| require f }
|
|
161
|
+
```
|
|
162
|
+
- [ ] Crear `spec/support/configuration_helper.rb`:
|
|
163
|
+
```ruby
|
|
164
|
+
module ConfigurationHelper
|
|
165
|
+
def with_config(**overrides)
|
|
166
|
+
original = DataDrain.configuration
|
|
167
|
+
DataDrain.configure { |c| overrides.each { |k, v| c.send("#{k}=", v) } }
|
|
168
|
+
yield
|
|
169
|
+
ensure
|
|
170
|
+
DataDrain.instance_variable_set(:@configuration, original)
|
|
171
|
+
DataDrain::Storage.reset_adapter!
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
RSpec.configure do |config|
|
|
176
|
+
config.include ConfigurationHelper
|
|
177
|
+
|
|
178
|
+
# Limpia la config global y el adapter cacheado entre tests para evitar leaks
|
|
179
|
+
# de estado. Tests que escriben archivos al disco deben usar `Dir.mktmpdir` o
|
|
180
|
+
# un `tmp/test_lake/` descartable; este hook NO los limpia (responsabilidad
|
|
181
|
+
# del test).
|
|
182
|
+
config.after(:each) do
|
|
183
|
+
DataDrain.reset_configuration!
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
- [ ] Commit: `chore: add spec support helpers`
|
|
188
|
+
|
|
189
|
+
### Checkpoint Fase 0
|
|
190
|
+
|
|
191
|
+
- [ ] `bundle exec rspec` pasa (4 tests + 0% nuevos)
|
|
192
|
+
- [ ] Coverage report generado
|
|
193
|
+
- [ ] 3 commits limpios en `feature/v0.2.0`
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Fase 1 — Item 2: Validación regex (P0)
|
|
198
|
+
|
|
199
|
+
**Roadmap:** [Item 2](../IMPROVEMENT_PLAN.md#item-2--validación-regex-de-table_name-primary_key-anti-sql-injection)
|
|
200
|
+
|
|
201
|
+
### 1.1 Implementar validación en Engine
|
|
202
|
+
|
|
203
|
+
- [ ] Editar `lib/data_drain/engine.rb`:
|
|
204
|
+
- Agregar constante de clase:
|
|
205
|
+
```ruby
|
|
206
|
+
IDENTIFIER_REGEX = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/.freeze
|
|
207
|
+
```
|
|
208
|
+
- En `#initialize`, después de capturar `@table_name` y `@primary_key`:
|
|
209
|
+
```ruby
|
|
210
|
+
validate_identifier!(:table_name, @table_name)
|
|
211
|
+
validate_identifier!(:primary_key, @primary_key)
|
|
212
|
+
```
|
|
213
|
+
- Agregar método privado:
|
|
214
|
+
```ruby
|
|
215
|
+
def validate_identifier!(name, value)
|
|
216
|
+
return if IDENTIFIER_REGEX.match?(value.to_s)
|
|
217
|
+
|
|
218
|
+
raise DataDrain::ConfigurationError,
|
|
219
|
+
"#{name} '#{value}' no es un identificador SQL válido"
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 1.2 Implementar validación en FileIngestor
|
|
224
|
+
|
|
225
|
+
- [ ] Editar `lib/data_drain/file_ingestor.rb`:
|
|
226
|
+
- Agregar misma constante o referenciar `Engine::IDENTIFIER_REGEX` (mejor: extraer a `DataDrain::Validations` módulo si vamos a tenerla en 2 clases)
|
|
227
|
+
- **Decisión:** extraer a módulo `lib/data_drain/validations.rb`:
|
|
228
|
+
```ruby
|
|
229
|
+
module DataDrain
|
|
230
|
+
module Validations
|
|
231
|
+
IDENTIFIER_REGEX = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/.freeze
|
|
232
|
+
|
|
233
|
+
module_function
|
|
234
|
+
|
|
235
|
+
def validate_identifier!(name, value)
|
|
236
|
+
return if IDENTIFIER_REGEX.match?(value.to_s)
|
|
237
|
+
|
|
238
|
+
raise DataDrain::ConfigurationError,
|
|
239
|
+
"#{name} '#{value}' no es un identificador SQL válido"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
- Require en `lib/data_drain.rb` después de `errors`
|
|
245
|
+
- En `Engine#initialize`: `Validations.validate_identifier!(:table_name, @table_name)`
|
|
246
|
+
- En `FileIngestor#initialize`: `Validations.validate_identifier!(:folder_name, @folder_name)`
|
|
247
|
+
|
|
248
|
+
### 1.3 Tests
|
|
249
|
+
|
|
250
|
+
- [ ] Agregar a `spec/data_drain/engine_spec.rb`:
|
|
251
|
+
```ruby
|
|
252
|
+
describe "validación de identificadores" do
|
|
253
|
+
it "rechaza table_name con punto y coma" do
|
|
254
|
+
expect {
|
|
255
|
+
DataDrain::Engine.new(valid_options.merge(table_name: "x; DROP TABLE y"))
|
|
256
|
+
}.to raise_error(DataDrain::ConfigurationError, /table_name.*no es un identificador/)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
it "rechaza primary_key con espacios" do
|
|
260
|
+
expect {
|
|
261
|
+
DataDrain::Engine.new(valid_options.merge(primary_key: "id desc"))
|
|
262
|
+
}.to raise_error(DataDrain::ConfigurationError)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "acepta identificador válido con guión bajo y números" do
|
|
266
|
+
expect {
|
|
267
|
+
DataDrain::Engine.new(valid_options.merge(table_name: "my_table_2"))
|
|
268
|
+
}.not_to raise_error
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
- [ ] Crear `spec/data_drain/validations_spec.rb`:
|
|
273
|
+
```ruby
|
|
274
|
+
RSpec.describe DataDrain::Validations do
|
|
275
|
+
describe ".validate_identifier!" do
|
|
276
|
+
it "no levanta para identificadores válidos" do
|
|
277
|
+
%w[users users_v2 _table TableName].each do |id|
|
|
278
|
+
expect { described_class.validate_identifier!(:x, id) }.not_to raise_error
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "levanta para identificadores inválidos" do
|
|
283
|
+
%w[1table table-name table.name].push("x; DROP", "").each do |id|
|
|
284
|
+
expect {
|
|
285
|
+
described_class.validate_identifier!(:x, id)
|
|
286
|
+
}.to raise_error(DataDrain::ConfigurationError)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 1.4 Validación local
|
|
294
|
+
|
|
295
|
+
- [ ] `bundle exec rspec spec/data_drain/validations_spec.rb spec/data_drain/engine_spec.rb`
|
|
296
|
+
- [ ] `bundle exec rubocop lib/data_drain/validations.rb lib/data_drain/engine.rb lib/data_drain/file_ingestor.rb`
|
|
297
|
+
|
|
298
|
+
### 1.5 Docs
|
|
299
|
+
|
|
300
|
+
- [ ] Actualizar `skill/references/antipatrones.md` item 13: agregar "Ahora la gema valida `table_name` y `primary_key` con regex; `select_sql` y `where_clause` siguen siendo trusted (no se validan)".
|
|
301
|
+
- [ ] Actualizar `skill/references/api-detallada.md` con nota sobre validación.
|
|
302
|
+
- [ ] Actualizar `CLAUDE.md` sección "Convenciones críticas" con regla nueva.
|
|
303
|
+
|
|
304
|
+
### 1.6 Commit
|
|
305
|
+
|
|
306
|
+
- [ ] `git add lib/data_drain/validations.rb lib/data_drain/engine.rb lib/data_drain/file_ingestor.rb lib/data_drain.rb`
|
|
307
|
+
- [ ] `git add spec/data_drain/validations_spec.rb spec/data_drain/engine_spec.rb`
|
|
308
|
+
- [ ] `git add skill/ CLAUDE.md`
|
|
309
|
+
- [ ] Commit: `feat(security): validar table_name/primary_key con regex (item 2)`
|
|
310
|
+
|
|
311
|
+
### Checkpoint Fase 1
|
|
312
|
+
|
|
313
|
+
- [ ] Tests verdes
|
|
314
|
+
- [ ] Rubocop limpio
|
|
315
|
+
- [ ] Coverage no bajó
|
|
316
|
+
- [ ] Identificadores comunes (`users`, `my_table`) siguen funcionando
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Fase 2 — Tests baseline
|
|
321
|
+
|
|
322
|
+
**Roadmap:** [Item 4 — parcial](../IMPROVEMENT_PLAN.md#item-4--cobertura-de-tests-p0-record-storage-gluerunner-observability)
|
|
323
|
+
|
|
324
|
+
Tests cheap sin dependencias externas. Establecen base de cobertura.
|
|
325
|
+
|
|
326
|
+
### 2.1 `configuration_spec.rb`
|
|
327
|
+
|
|
328
|
+
- [ ] Crear `spec/data_drain/configuration_spec.rb`
|
|
329
|
+
- [ ] Tests:
|
|
330
|
+
- Defaults correctos (`storage_mode == :local`, `batch_size == 5000`, etc.)
|
|
331
|
+
- `duckdb_connection_string` formato URI con `idle_in_transaction_session_timeout` interpolado
|
|
332
|
+
- `idle_in_transaction_session_timeout = 0` no se omite (se incluye en URI como `0`)
|
|
333
|
+
- [ ] Validar: `bundle exec rspec spec/data_drain/configuration_spec.rb`
|
|
334
|
+
|
|
335
|
+
### 2.2 `observability_spec.rb`
|
|
336
|
+
|
|
337
|
+
- [ ] Crear `spec/data_drain/observability_spec.rb`
|
|
338
|
+
- [ ] Crear clase de test:
|
|
339
|
+
```ruby
|
|
340
|
+
class TestComponent
|
|
341
|
+
include DataDrain::Observability
|
|
342
|
+
attr_accessor :logger
|
|
343
|
+
def emit(level, event, meta = {}); safe_log(level, event, meta); end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
- [ ] Tests:
|
|
347
|
+
- `safe_log` no-op si `@logger` nil
|
|
348
|
+
- Formato KV con `component=` `event=` primero
|
|
349
|
+
- Filtra `password`, `token`, `secret`, `api_key`, `auth` (versión actual; item 9 ampliará)
|
|
350
|
+
- `rescue StandardError` no propaga (logger que levanta)
|
|
351
|
+
- `exception_metadata` trunca a 200, escapa `"`
|
|
352
|
+
- `observability_name` extrae primer namespace en snake_case
|
|
353
|
+
- Funciona con `extend` (clase con `@logger` de clase)
|
|
354
|
+
- [ ] Validar: `bundle exec rspec spec/data_drain/observability_spec.rb`
|
|
355
|
+
|
|
356
|
+
### 2.3 `types/json_type_spec.rb`
|
|
357
|
+
|
|
358
|
+
- [ ] Crear `spec/data_drain/types/json_type_spec.rb`
|
|
359
|
+
- [ ] Tests:
|
|
360
|
+
- `cast(nil)` → nil
|
|
361
|
+
- `cast({"a" => 1})` → `{"a" => 1}`
|
|
362
|
+
- `cast([1,2,3])` → `[1,2,3]`
|
|
363
|
+
- `cast('{"a":1}')` → `{"a" => 1}`
|
|
364
|
+
- `cast("not json")` → `"not json"` (no levanta)
|
|
365
|
+
- [ ] Validar
|
|
366
|
+
|
|
367
|
+
### 2.4 `storage_spec.rb` (factory)
|
|
368
|
+
|
|
369
|
+
- [ ] Crear `spec/data_drain/storage_spec.rb`
|
|
370
|
+
- [ ] Tests:
|
|
371
|
+
- `Storage.adapter` con `:local` retorna `Local` instance
|
|
372
|
+
- `Storage.adapter` con `:s3` retorna `S3` instance
|
|
373
|
+
- `Storage.adapter` con `:foo` levanta `InvalidAdapterError`
|
|
374
|
+
- `Storage.adapter` cachea (misma instancia entre llamadas)
|
|
375
|
+
- `Storage.reset_adapter!` invalida cache
|
|
376
|
+
- Después de `reset_adapter!`, próxima llamada usa nuevo `storage_mode`
|
|
377
|
+
- [ ] Validar
|
|
378
|
+
|
|
379
|
+
### 2.5 Commit
|
|
380
|
+
|
|
381
|
+
- [ ] `git add spec/data_drain/configuration_spec.rb spec/data_drain/observability_spec.rb spec/data_drain/types/ spec/data_drain/storage_spec.rb`
|
|
382
|
+
- [ ] Commit: `test: cobertura baseline (Configuration, Observability, JsonType, Storage factory)`
|
|
383
|
+
|
|
384
|
+
### Checkpoint Fase 2
|
|
385
|
+
|
|
386
|
+
- [ ] `bundle exec rspec` corre todos los specs sin error
|
|
387
|
+
- [ ] Coverage report sube ≥ 50%
|
|
388
|
+
- [ ] Tiempo total de suite < 5s
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Fase 3 — Item 1: S3 credential_chain (P0)
|
|
393
|
+
|
|
394
|
+
**Roadmap:** [Item 1](../IMPROVEMENT_PLAN.md#item-1--migrar-credenciales-s3-a-credential_chain-de-duckdb)
|
|
395
|
+
|
|
396
|
+
### 3.1 Investigación previa
|
|
397
|
+
|
|
398
|
+
- [ ] Verificar versión DuckDB en uso:
|
|
399
|
+
```bash
|
|
400
|
+
bundle exec ruby -e "require 'duckdb'; puts DuckDB::LIBRARY_VERSION"
|
|
401
|
+
```
|
|
402
|
+
Debe ser ≥ 0.10. Si no, bumpear `Gemfile`.
|
|
403
|
+
- [ ] Probar manualmente `CREATE SECRET` en consola DuckDB:
|
|
404
|
+
```bash
|
|
405
|
+
bin/console
|
|
406
|
+
> db = DuckDB::Database.open(":memory:"); conn = db.connect
|
|
407
|
+
> conn.query("INSTALL httpfs; LOAD httpfs;")
|
|
408
|
+
> conn.query("CREATE SECRET test (TYPE S3, PROVIDER credential_chain, REGION 'us-east-1');")
|
|
409
|
+
> conn.query("FROM duckdb_secrets();")
|
|
410
|
+
```
|
|
411
|
+
- [ ] Si AWS env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) están seteadas, probar listar un bucket de prueba:
|
|
412
|
+
```ruby
|
|
413
|
+
conn.query("FROM read_parquet('s3://your-test-bucket/path/*.parquet') LIMIT 1")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### 3.2 Refactor `Storage::S3#setup_duckdb`
|
|
417
|
+
|
|
418
|
+
- [ ] Editar `lib/data_drain/storage/s3.rb`:
|
|
419
|
+
```ruby
|
|
420
|
+
def setup_duckdb(connection)
|
|
421
|
+
connection.query("INSTALL httpfs; LOAD httpfs;")
|
|
422
|
+
create_s3_secret(connection)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
private
|
|
426
|
+
|
|
427
|
+
def create_s3_secret(connection)
|
|
428
|
+
region = @config.aws_region
|
|
429
|
+
raise DataDrain::ConfigurationError, "aws_region es obligatorio para storage_mode=:s3" if region.nil?
|
|
430
|
+
|
|
431
|
+
safe_region = escape_sql(region)
|
|
432
|
+
|
|
433
|
+
if @config.aws_access_key_id && @config.aws_secret_access_key
|
|
434
|
+
connection.query(<<~SQL)
|
|
435
|
+
CREATE OR REPLACE SECRET data_drain_s3 (
|
|
436
|
+
TYPE S3,
|
|
437
|
+
KEY_ID '#{escape_sql(@config.aws_access_key_id)}',
|
|
438
|
+
SECRET '#{escape_sql(@config.aws_secret_access_key)}',
|
|
439
|
+
REGION '#{safe_region}'
|
|
440
|
+
);
|
|
441
|
+
SQL
|
|
442
|
+
else
|
|
443
|
+
connection.query(<<~SQL)
|
|
444
|
+
CREATE OR REPLACE SECRET data_drain_s3 (
|
|
445
|
+
TYPE S3,
|
|
446
|
+
PROVIDER credential_chain,
|
|
447
|
+
REGION '#{safe_region}'
|
|
448
|
+
);
|
|
449
|
+
SQL
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# NOTA: aws_region también pasa por escape_sql (review big-pickle issue 1).
|
|
454
|
+
# Aunque AWS no permite `'` en region names, defendemos en profundidad.
|
|
455
|
+
def escape_sql(value)
|
|
456
|
+
value.to_s.gsub("'", "''")
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 3.3 Tests `storage/s3_spec.rb`
|
|
461
|
+
|
|
462
|
+
- [ ] Crear `spec/data_drain/storage/s3_spec.rb`
|
|
463
|
+
- [ ] Setup: usar `aws-sdk-s3` con `Aws::S3::Client.new(stub_responses: true)` para `destroy_partitions`
|
|
464
|
+
- [ ] Mockear DuckDB connection (objeto que registra queries):
|
|
465
|
+
```ruby
|
|
466
|
+
let(:duckdb_conn) do
|
|
467
|
+
queries = []
|
|
468
|
+
Class.new do
|
|
469
|
+
define_method(:query) { |q| queries << q }
|
|
470
|
+
define_method(:queries) { queries }
|
|
471
|
+
end.new
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
- [ ] Tests:
|
|
475
|
+
- `setup_duckdb` con `aws_access_key_id` seteado → emite `CREATE SECRET ... KEY_ID ... SECRET`
|
|
476
|
+
- `setup_duckdb` sin credenciales → emite `CREATE SECRET ... PROVIDER credential_chain`
|
|
477
|
+
- Sin `aws_region` levanta `ConfigurationError`
|
|
478
|
+
- `escape_sql` duplica comillas simples
|
|
479
|
+
- `aws_region` con `'` (caso patológico) también se escapa en el `CREATE SECRET` (no rompe SQL)
|
|
480
|
+
- `build_path` retorna `s3://...`
|
|
481
|
+
- `destroy_partitions` con stub de S3:
|
|
482
|
+
- prefix correcto
|
|
483
|
+
- regex matching
|
|
484
|
+
- delete_objects llamado en lotes de 1000 cuando hay > 1000
|
|
485
|
+
- retorna count
|
|
486
|
+
|
|
487
|
+
### 3.4 Validación local
|
|
488
|
+
|
|
489
|
+
- [ ] `bundle exec rspec spec/data_drain/storage/s3_spec.rb`
|
|
490
|
+
- [ ] `bundle exec rubocop lib/data_drain/storage/s3.rb`
|
|
491
|
+
|
|
492
|
+
### 3.5 Docs
|
|
493
|
+
|
|
494
|
+
- [ ] Actualizar `skill/references/api-detallada.md` sección Storage::S3
|
|
495
|
+
- [ ] Actualizar `skill/SKILL.md` glosario y FAQ (mencionar credential_chain como default)
|
|
496
|
+
- [ ] Actualizar `README.md` sección Configuración: aclarar que `aws_access_key_id`/`aws_secret_access_key` son opcionales si IAM rol o env vars
|
|
497
|
+
- [ ] Actualizar `CLAUDE.md` sección "Seguridad"
|
|
498
|
+
|
|
499
|
+
### 3.6 Commit
|
|
500
|
+
|
|
501
|
+
- [ ] `git add lib/data_drain/storage/s3.rb spec/data_drain/storage/s3_spec.rb`
|
|
502
|
+
- [ ] `git add skill/ README.md CLAUDE.md`
|
|
503
|
+
- [ ] Commit: `security(s3): migrar a CREATE SECRET con credential_chain (item 1)`
|
|
504
|
+
|
|
505
|
+
### Checkpoint Fase 3
|
|
506
|
+
|
|
507
|
+
- [ ] Tests S3 verdes (mockeados)
|
|
508
|
+
- [ ] Test manual con bucket real (si hay): consulta de un Parquet vía `Record.where`
|
|
509
|
+
- [ ] Coverage sube
|
|
510
|
+
- [ ] Documentado backward-compat
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Fase 4 — Tests Storage::Local + GlueRunner
|
|
515
|
+
|
|
516
|
+
### 4.1 `storage/local_spec.rb`
|
|
517
|
+
|
|
518
|
+
- [ ] Crear `spec/data_drain/storage/local_spec.rb`
|
|
519
|
+
- [ ] Usar `Dir.mktmpdir` por test para aislamiento
|
|
520
|
+
- [ ] Tests:
|
|
521
|
+
- `prepare_export_path` crea directorio anidado
|
|
522
|
+
- `build_path` con/sin `partition_path`
|
|
523
|
+
- `destroy_partitions` con todas las keys → borra directorio específico
|
|
524
|
+
- `destroy_partitions` con keys parciales → wildcard glob
|
|
525
|
+
- `destroy_partitions` retorna count correcto
|
|
526
|
+
- `destroy_partitions` con pattern que no matchea → retorna 0
|
|
527
|
+
|
|
528
|
+
### 4.2 `glue_runner_spec.rb`
|
|
529
|
+
|
|
530
|
+
- [ ] Crear `spec/data_drain/glue_runner_spec.rb`
|
|
531
|
+
- [ ] Usar `Aws::Glue::Client.new(stub_responses: true)` y stubs para `start_job_run`/`get_job_run`
|
|
532
|
+
- [ ] Tests:
|
|
533
|
+
- SUCCEEDED inmediato → retorna `true`
|
|
534
|
+
- RUNNING → SUCCEEDED → retorna `true` (test con polling_interval bajísimo o stubear sleep)
|
|
535
|
+
- FAILED → levanta RuntimeError con mensaje
|
|
536
|
+
- STOPPED → idem
|
|
537
|
+
- TIMEOUT → idem
|
|
538
|
+
- `error_message` truncado a 200 chars + escape de `"`
|
|
539
|
+
- Logs emitidos: `glue_runner.start`, `glue_runner.polling`, `glue_runner.complete|failed`
|
|
540
|
+
- **Para evitar sleep real:** stubear `Kernel.sleep` o mover a `class << self; def polling_sleep(s); sleep s; end; end` y stubear
|
|
541
|
+
|
|
542
|
+
### 4.3 Validación + commit
|
|
543
|
+
|
|
544
|
+
- [ ] `bundle exec rspec spec/data_drain/storage/local_spec.rb spec/data_drain/glue_runner_spec.rb`
|
|
545
|
+
- [ ] Commit: `test: cobertura Storage::Local y GlueRunner`
|
|
546
|
+
|
|
547
|
+
### Checkpoint Fase 4
|
|
548
|
+
|
|
549
|
+
- [ ] Coverage > 70%
|
|
550
|
+
- [ ] Tiempo de suite < 10s
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Fase 5 — Item 3: `Record.disconnect!` + tests Record
|
|
555
|
+
|
|
556
|
+
**Roadmap:** [Item 3](../IMPROVEMENT_PLAN.md#item-3--cleanup-de-conexión-duckdb-thread-local)
|
|
557
|
+
|
|
558
|
+
### 5.1 Implementar `Record.disconnect!`
|
|
559
|
+
|
|
560
|
+
- [ ] Editar `lib/data_drain/record.rb`:
|
|
561
|
+
```ruby
|
|
562
|
+
# Cierra la conexión DuckDB del thread actual y limpia Thread.current.
|
|
563
|
+
# Idempotente: llamarlo varias veces no levanta.
|
|
564
|
+
#
|
|
565
|
+
# Útil en middlewares de Sidekiq/Puma para evitar memory leak en threads
|
|
566
|
+
# de larga vida.
|
|
567
|
+
#
|
|
568
|
+
# @return [void]
|
|
569
|
+
def self.disconnect!
|
|
570
|
+
entry = Thread.current.delete(:data_drain_duckdb)
|
|
571
|
+
return unless entry
|
|
572
|
+
|
|
573
|
+
entry[:conn]&.close
|
|
574
|
+
entry[:db]&.close
|
|
575
|
+
rescue StandardError
|
|
576
|
+
# silencio en cleanup
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 5.2 Tests `record_spec.rb`
|
|
581
|
+
|
|
582
|
+
- [ ] Crear `spec/data_drain/record_spec.rb`
|
|
583
|
+
- [ ] Definir clase de test:
|
|
584
|
+
```ruby
|
|
585
|
+
class TestArchived < DataDrain::Record
|
|
586
|
+
self.bucket = "spec/fixtures"
|
|
587
|
+
self.folder_name = "test_archive"
|
|
588
|
+
self.partition_keys = [:year, :month]
|
|
589
|
+
|
|
590
|
+
attribute :id, :string
|
|
591
|
+
attribute :value, :integer
|
|
592
|
+
attribute :created_at, :datetime
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
- [ ] Generar fixtures Parquet en `spec/fixtures/test_archive/year=2026/month=3/data.parquet` usando DuckDB:
|
|
596
|
+
```ruby
|
|
597
|
+
# spec/support/parquet_fixtures.rb
|
|
598
|
+
module ParquetFixtures
|
|
599
|
+
def self.generate!
|
|
600
|
+
path = "spec/fixtures/test_archive"
|
|
601
|
+
FileUtils.rm_rf(path)
|
|
602
|
+
db = DuckDB::Database.open(":memory:")
|
|
603
|
+
conn = db.connect
|
|
604
|
+
conn.query(<<~SQL)
|
|
605
|
+
COPY (
|
|
606
|
+
SELECT 'uuid-1' AS id, 100 AS value, TIMESTAMP '2026-03-01' AS created_at, 2026 AS year, 3 AS month
|
|
607
|
+
UNION ALL SELECT 'uuid-2', 200, TIMESTAMP '2026-03-15', 2026, 3
|
|
608
|
+
) TO '#{path}' (FORMAT PARQUET, PARTITION_BY (year, month), OVERWRITE_OR_IGNORE 1);
|
|
609
|
+
SQL
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
```
|
|
613
|
+
- [ ] **Orden de carga (review big-pickle issue 4):** `spec/support/*.rb` se carga vía `Dir[...].sort.each` (Fase 0.5) ANTES de los tests, pero el bloque `before(:suite)` que dispara fixtures debe registrarse en el spec_helper, no en el archivo de support. Hacerlo así:
|
|
614
|
+
```ruby
|
|
615
|
+
# spec/spec_helper.rb (al final, después de Dir[...].sort.each)
|
|
616
|
+
RSpec.configure do |config|
|
|
617
|
+
config.before(:suite) do
|
|
618
|
+
ParquetFixtures.generate! # genera fixtures Parquet
|
|
619
|
+
# Si hay setup global de DataDrain (configure), va antes de generate!
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
```
|
|
623
|
+
Razón: si `ParquetFixtures.generate!` se invoca en `support/parquet_fixtures.rb` directamente, se ejecuta en tiempo de `require` (antes de cualquier `before(:suite)` de spec_helper). Manteniendo solo la definición del módulo en `support/` y el trigger en `spec_helper.rb`, garantizamos orden.
|
|
624
|
+
- [ ] Tests:
|
|
625
|
+
- `.where(year: 2026, month: 3)` retorna 2 instancias
|
|
626
|
+
- `.where(year: 2026, month: 3, limit: 1)` retorna 1
|
|
627
|
+
- `.find("uuid-1", year: 2026, month: 3)` retorna instancia con value 100
|
|
628
|
+
- `.find("nonexistent", year: 2026, month: 3)` retorna nil
|
|
629
|
+
- `.find("foo' OR 1=1 --", year: 2026, month: 3)` retorna nil (sanitización)
|
|
630
|
+
- `.where(year: 2099, month: 12)` retorna `[]` (Parquet no existe, no levanta)
|
|
631
|
+
- `build_query_path` respeta orden `[:year, :month]` aunque kwargs sean `(month: 3, year: 2026)`
|
|
632
|
+
- `.connection` retorna instancia, segunda llamada en mismo thread retorna misma
|
|
633
|
+
- `.connection` en thread distinto retorna instancia distinta
|
|
634
|
+
- `.disconnect!` limpia `Thread.current[:data_drain_duckdb]`
|
|
635
|
+
- `.disconnect!` llamado dos veces no levanta
|
|
636
|
+
- Después de `.disconnect!`, `.connection` reabre
|
|
637
|
+
|
|
638
|
+
### 5.3 Validación
|
|
639
|
+
|
|
640
|
+
- [ ] `bundle exec rspec spec/data_drain/record_spec.rb`
|
|
641
|
+
- [ ] `bundle exec rubocop lib/data_drain/record.rb`
|
|
642
|
+
|
|
643
|
+
### 5.4 Docs
|
|
644
|
+
|
|
645
|
+
- [ ] Actualizar `CLAUDE.md` sección "Conexiones thread-local" con snippet Sidekiq middleware:
|
|
646
|
+
```ruby
|
|
647
|
+
# config/initializers/sidekiq.rb
|
|
648
|
+
Sidekiq.configure_server do |config|
|
|
649
|
+
config.server_middleware do |chain|
|
|
650
|
+
chain.add Class.new {
|
|
651
|
+
def call(_worker, _job, _queue)
|
|
652
|
+
yield
|
|
653
|
+
ensure
|
|
654
|
+
DataDrain::Record.disconnect!
|
|
655
|
+
end
|
|
656
|
+
}
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
```
|
|
660
|
+
- [ ] Actualizar `skill/references/api-detallada.md` sección Record.disconnect!
|
|
661
|
+
- [ ] Actualizar `skill/references/antipatrones.md` item 12 (ahora hay forma correcta de cerrar)
|
|
662
|
+
|
|
663
|
+
### 5.5 Commit
|
|
664
|
+
|
|
665
|
+
- [ ] `git add lib/data_drain/record.rb spec/data_drain/record_spec.rb spec/support/parquet_fixtures.rb`
|
|
666
|
+
- [ ] `git add CLAUDE.md skill/`
|
|
667
|
+
- [ ] Commit: `feat(record): agregar Record.disconnect! para cleanup thread-local (item 3)`
|
|
668
|
+
|
|
669
|
+
### Checkpoint Fase 5
|
|
670
|
+
|
|
671
|
+
- [ ] Tests Record verdes
|
|
672
|
+
- [ ] Fixtures Parquet generados en `before(:suite)`
|
|
673
|
+
- [ ] Documentación de uso Sidekiq publicada
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Fase 6 — Tests Engine + FileIngestor
|
|
678
|
+
|
|
679
|
+
Refuerzo y expansión de specs existentes.
|
|
680
|
+
|
|
681
|
+
### 6.1 Engine
|
|
682
|
+
|
|
683
|
+
- [ ] Revisar `spec/data_drain/engine_spec.rb`. Agregar:
|
|
684
|
+
- Test `pg_count == 0` → retorna `true` sin export ni purge, log `engine.skip_empty`
|
|
685
|
+
- Test `skip_export: true` → no llama `export_to_parquet`, sí llama `verify_integrity`
|
|
686
|
+
- Test `verify_integrity` retorna false → no llama `purge_from_postgres`, retorna false
|
|
687
|
+
- Test heartbeat: 100 lotes simulados → emite `engine.purge_heartbeat`
|
|
688
|
+
- Test `throttle_delay > 0` → llama sleep entre lotes (stubear Kernel.sleep)
|
|
689
|
+
- Test `idle_in_transaction_session_timeout = 0` se setea
|
|
690
|
+
- Test `idle_in_transaction_session_timeout = nil` no se setea
|
|
691
|
+
|
|
692
|
+
### 6.2 FileIngestor
|
|
693
|
+
|
|
694
|
+
- [ ] Revisar `spec/data_drain/file_ingestor_spec.rb`. Agregar:
|
|
695
|
+
- Test archivo no existe → retorna false, log `file_ingestor.file_not_found`
|
|
696
|
+
- Test count == 0 → cleanup + retorna true (con `delete_after_upload: true` borra)
|
|
697
|
+
- Test JSON → usa `read_json_auto`
|
|
698
|
+
- Test Parquet → usa `read_parquet`
|
|
699
|
+
- Test extensión no soportada → levanta `DataDrain::Error`
|
|
700
|
+
- Test `delete_after_upload: false` → archivo no se borra
|
|
701
|
+
|
|
702
|
+
### 6.3 Validación + commit
|
|
703
|
+
|
|
704
|
+
- [ ] `bundle exec rspec`
|
|
705
|
+
- [ ] Coverage ≥ 80%
|
|
706
|
+
- [ ] Commit: `test: expandir cobertura Engine y FileIngestor`
|
|
707
|
+
|
|
708
|
+
### Checkpoint Fase 6
|
|
709
|
+
|
|
710
|
+
- [ ] Coverage ≥ 80% líneas (criterio del item 4)
|
|
711
|
+
- [ ] Tiempo suite total < 30s
|
|
712
|
+
- [ ] No flakes en 3 corridas seguidas: `for i in 1 2 3; do bundle exec rspec || break; done`
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Fase 7 — Release
|
|
717
|
+
|
|
718
|
+
### 7.1 Lint global
|
|
719
|
+
|
|
720
|
+
- [ ] `bundle exec rubocop` sin ofensas en archivos modificados
|
|
721
|
+
- [ ] Si rubocop reporta cosas en archivos NO tocados, dejar como están (regla: no flag código no tocado)
|
|
722
|
+
|
|
723
|
+
### 7.2 Coverage final
|
|
724
|
+
|
|
725
|
+
- [ ] `bundle exec rspec` y verificar `coverage/index.html`
|
|
726
|
+
- [ ] Anotar % en CHANGELOG
|
|
727
|
+
|
|
728
|
+
### 7.3 CHANGELOG
|
|
729
|
+
|
|
730
|
+
- [ ] Editar `CHANGELOG.md`. Agregar al tope:
|
|
731
|
+
```markdown
|
|
732
|
+
## [0.2.0] - 2026-XX-XX
|
|
733
|
+
|
|
734
|
+
### Security
|
|
735
|
+
- **BREAKING (preventivo):** `table_name` y `primary_key` se validan contra regex `\A[a-zA-Z_][a-zA-Z0-9_]*\z`. Identificadores con caracteres especiales (puntos, espacios, comillas) ahora levantan `DataDrain::ConfigurationError`. (item 2)
|
|
736
|
+
- Storage::S3 migra a `CREATE SECRET (TYPE S3, PROVIDER credential_chain)`. Si `aws_access_key_id`/`aws_secret_access_key` están seteados, se mantiene comportamiento explícito; si no, usa AWS credential chain (IAM roles, env vars, ~/.aws/credentials). (item 1)
|
|
737
|
+
|
|
738
|
+
### Features
|
|
739
|
+
- `Record.disconnect!` cierra y limpia la conexión DuckDB thread-local. Recomendado en middlewares Sidekiq/Puma para evitar memory leak. Idempotente. (item 3)
|
|
740
|
+
|
|
741
|
+
### Tests
|
|
742
|
+
- Cobertura 4 specs → ~XX specs (Record, Storage::Local, Storage::S3, GlueRunner, Observability, Configuration, JsonType, Validations).
|
|
743
|
+
- Cobertura líneas: ~XX% (medida con SimpleCov, mínimo 80%).
|
|
744
|
+
```
|
|
745
|
+
- [ ] Reemplazar fecha y porcentajes reales
|
|
746
|
+
|
|
747
|
+
### 7.4 Bump de versión
|
|
748
|
+
|
|
749
|
+
- [ ] Editar `lib/data_drain/version.rb`: `VERSION = "0.2.0"`
|
|
750
|
+
- [ ] `bundle install` (actualiza Gemfile.lock)
|
|
751
|
+
|
|
752
|
+
### 7.5 Skill regenerada
|
|
753
|
+
|
|
754
|
+
- [ ] Invocar `skill-builder` en modo completo para regenerar `skill/` con cambios:
|
|
755
|
+
```
|
|
756
|
+
Modo: completo (gem-release lo dispara automáticamente)
|
|
757
|
+
```
|
|
758
|
+
O ejecutar manualmente actualizaciones de `skill/SKILL.md` y `references/`.
|
|
759
|
+
|
|
760
|
+
### 7.6 Commit final del release
|
|
761
|
+
|
|
762
|
+
- [ ] `git add CHANGELOG.md lib/data_drain/version.rb Gemfile.lock skill/`
|
|
763
|
+
- [ ] Commit: `chore: release v0.2.0 — hardening de seguridad y testing`
|
|
764
|
+
|
|
765
|
+
### 7.7 Tag y push
|
|
766
|
+
|
|
767
|
+
- [ ] `git tag v0.2.0`
|
|
768
|
+
- [ ] `git push origin feature/v0.2.0`
|
|
769
|
+
- [ ] `git push origin v0.2.0`
|
|
770
|
+
- [ ] Crear PR a `main` (vía `gh pr create`) con cuerpo basado en CHANGELOG
|
|
771
|
+
|
|
772
|
+
### 7.8 Post-merge
|
|
773
|
+
|
|
774
|
+
- [ ] Mergear PR
|
|
775
|
+
- [ ] Si aplica: invocar skill `gem-release` para empaquetar
|
|
776
|
+
- [ ] Actualizar `docs/IMPROVEMENT_PLAN.md` marcando items 1, 2, 3, 4 como `[x]`
|
|
777
|
+
- [ ] Mover este plan a `docs/execution/archive/v0.2.0.md`
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Validación final del release
|
|
782
|
+
|
|
783
|
+
- [ ] Tests verdes en CI (cuando exista, item 14) o local
|
|
784
|
+
- [ ] Coverage ≥ 80%
|
|
785
|
+
- [ ] Rubocop sin ofensas
|
|
786
|
+
- [ ] CHANGELOG completo
|
|
787
|
+
- [ ] Version bumped
|
|
788
|
+
- [ ] Tag creado
|
|
789
|
+
- [ ] PR mergeado
|
|
790
|
+
- [ ] Items 1-4 marcados `[x]` en roadmap
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## Plan B: si algún item se atasca
|
|
795
|
+
|
|
796
|
+
| Si... | Entonces... |
|
|
797
|
+
|-------|-------------|
|
|
798
|
+
| Item 1 (S3 secret) requiere DuckDB > 1.4 | Bumpear `Gemfile` o documentar y postergar a v0.3.0 |
|
|
799
|
+
| Tests Record requieren Postgres real | Marcar como `:integration`, skip en suite default |
|
|
800
|
+
| Tests con `Aws::S3::Client.stub_responses` no soportan `list_objects_v2` paginado | Usar `WebMock` con respuestas XML reales de S3 |
|
|
801
|
+
| Coverage no llega a 80% | Bajar umbral a 70% en este release, item 4 sigue abierto en v0.2.1 |
|
|
802
|
+
| Rubocop versión actual reporta nuevas ofensas en código tocado | Fix puntual o `# rubocop:disable` con razón en comentario |
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## Notas para el agente que ejecuta
|
|
807
|
+
|
|
808
|
+
- **Cada commit debe ser autocontenido y atómico.** Si un cambio rompe tests, no commit.
|
|
809
|
+
- **Antes de commit, correr `rspec` Y `rubocop`** para los archivos tocados.
|
|
810
|
+
- **No crear archivos no listados** sin pedir confirmación al usuario.
|
|
811
|
+
- **Si surge bloqueo, marcar el subtask como `[!]` y consultar al usuario** antes de tomar shortcut.
|
|
812
|
+
- **No saltarse fases.** El orden está pensado para minimizar riesgo y maximizar cobertura.
|