appydave-tools 0.36.0 → 0.37.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/bin/dam +91 -7
- data/docs/ai-instructions/code-quality-retrospective.md +402 -0
- data/docs/ai-instructions/defensive-logging-audit.md +802 -0
- data/lib/appydave/tools/configuration/config.rb +7 -1
- data/lib/appydave/tools/dam/project_resolver.rb +7 -5
- data/lib/appydave/tools/version.rb +1 -1
- data/package.json +1 -1
- metadata +3 -1
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
# Defensive Logging Strategy Audit
|
|
2
|
+
|
|
3
|
+
## Objective
|
|
4
|
+
Ensure the codebase has sufficient logging infrastructure for remote debugging, especially around nil exceptions and configuration issues. This is particularly critical for internal tools used by remote team members where configuration problems are hard to diagnose.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
This is a Ruby gem project (`appydave-tools`) that provides CLI tools for YouTube content creation workflows. It's used by multiple team members with different system configurations. Recent issues:
|
|
8
|
+
|
|
9
|
+
- **The Jan Problem:** Remote user had configuration issues that were hard to debug because errors were opaque ("undefined method for nil:NilClass" without context about which configuration value was nil or where it was accessed)
|
|
10
|
+
- **Configuration Hell:** Multiple configuration sources (settings.json, channels.json, .env files) make it hard to trace where values come from
|
|
11
|
+
- **Path Resolution:** Brand/project path logic fails silently when paths don't exist or are misconfigured
|
|
12
|
+
|
|
13
|
+
## Why This Matters
|
|
14
|
+
**Ruby's nil exceptions are the worst:** `undefined method 'X' for nil:NilClass` tells you almost nothing about:
|
|
15
|
+
- Which object was nil
|
|
16
|
+
- Where it was supposed to be set
|
|
17
|
+
- What configuration or method call led to the nil
|
|
18
|
+
- What the system state was at the time
|
|
19
|
+
|
|
20
|
+
**Defensive logging solves this:** With proper logging, errors become: "Failed to resolve brand 'appydave': settings.json missing video-projects-root at line 23 in BrandResolver#resolve_path"
|
|
21
|
+
|
|
22
|
+
## Analysis Process
|
|
23
|
+
|
|
24
|
+
### Phase 1: Configuration Loading Audit
|
|
25
|
+
|
|
26
|
+
**Focus areas:**
|
|
27
|
+
- `lib/appydave/tools/configuration/` (all config loading)
|
|
28
|
+
- DAM configuration (settings.json parsing)
|
|
29
|
+
- Channel configuration (channels.json parsing)
|
|
30
|
+
- Environment variables (.env loading)
|
|
31
|
+
|
|
32
|
+
**What to check:**
|
|
33
|
+
|
|
34
|
+
1. **Configuration file loading:**
|
|
35
|
+
```ruby
|
|
36
|
+
# ❌ BAD: Silent failure
|
|
37
|
+
def load_config
|
|
38
|
+
JSON.parse(File.read(config_path))
|
|
39
|
+
rescue => e
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ✅ GOOD: Logged failure with context
|
|
44
|
+
def load_config
|
|
45
|
+
log.debug("Loading config from: #{config_path}")
|
|
46
|
+
config = JSON.parse(File.read(config_path))
|
|
47
|
+
log.debug("Config loaded successfully: #{config.keys.join(', ')}")
|
|
48
|
+
config
|
|
49
|
+
rescue Errno::ENOENT => e
|
|
50
|
+
log.error("Config file not found: #{config_path}")
|
|
51
|
+
log.error("Current directory: #{Dir.pwd}")
|
|
52
|
+
log.error("Expected location: #{File.expand_path(config_path)}")
|
|
53
|
+
raise ConfigurationError, "Missing config file: #{config_path}"
|
|
54
|
+
rescue JSON::ParserError => e
|
|
55
|
+
log.error("Invalid JSON in config file: #{config_path}")
|
|
56
|
+
log.error("Parse error: #{e.message}")
|
|
57
|
+
log.error("Check file syntax at: #{config_path}")
|
|
58
|
+
raise ConfigurationError, "Invalid JSON in #{config_path}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
2. **Configuration value access:**
|
|
63
|
+
```ruby
|
|
64
|
+
# ❌ BAD: Will blow up with opaque nil error
|
|
65
|
+
def video_projects_root
|
|
66
|
+
config['video-projects-root']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ✅ GOOD: Explicit nil check with helpful error
|
|
70
|
+
def video_projects_root
|
|
71
|
+
value = config['video-projects-root']
|
|
72
|
+
if value.nil?
|
|
73
|
+
log.error("Missing 'video-projects-root' in settings.json")
|
|
74
|
+
log.error("Available keys: #{config.keys.join(', ')}")
|
|
75
|
+
log.error("Config file location: #{config_path}")
|
|
76
|
+
raise ConfigurationError, "Missing 'video-projects-root' in settings.json. Run 'ad_config -e' to set it."
|
|
77
|
+
end
|
|
78
|
+
log.debug("video-projects-root: #{value}")
|
|
79
|
+
value
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. **Configuration echo on startup:**
|
|
84
|
+
```ruby
|
|
85
|
+
# ✅ GOOD: Log loaded configuration (sanitized)
|
|
86
|
+
def initialize
|
|
87
|
+
log.debug("=== Configuration Loaded ===")
|
|
88
|
+
log.debug("Config file: #{config_path}")
|
|
89
|
+
log.debug("Settings: #{sanitize_config(config).inspect}")
|
|
90
|
+
log.debug("Environment: #{ENV['DAM_DEBUG'] ? 'DEBUG' : 'PRODUCTION'}")
|
|
91
|
+
log.debug("===========================")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sanitize_config(config)
|
|
95
|
+
# Remove sensitive values before logging
|
|
96
|
+
config.reject { |k, _| k.include?('token') || k.include?('secret') }
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Phase 2: Path Resolution Audit
|
|
101
|
+
|
|
102
|
+
**Focus areas:**
|
|
103
|
+
- Brand resolution (appydave → v-appydave)
|
|
104
|
+
- Project path construction
|
|
105
|
+
- File path validation
|
|
106
|
+
- Directory existence checks
|
|
107
|
+
|
|
108
|
+
**What to check:**
|
|
109
|
+
|
|
110
|
+
1. **Brand resolution:**
|
|
111
|
+
```ruby
|
|
112
|
+
# ❌ BAD: Silent failure if brand not found
|
|
113
|
+
def resolve_brand(brand_name)
|
|
114
|
+
BRAND_MAP[brand_name]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# ✅ GOOD: Logged resolution with fallback
|
|
118
|
+
def resolve_brand(brand_name)
|
|
119
|
+
log.debug("Resolving brand: #{brand_name}")
|
|
120
|
+
|
|
121
|
+
# Try exact match
|
|
122
|
+
if BRAND_MAP.key?(brand_name)
|
|
123
|
+
result = BRAND_MAP[brand_name]
|
|
124
|
+
log.debug("Brand resolved (exact): #{brand_name} → #{result}")
|
|
125
|
+
return result
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Try case-insensitive
|
|
129
|
+
key = BRAND_MAP.keys.find { |k| k.downcase == brand_name.downcase }
|
|
130
|
+
if key
|
|
131
|
+
result = BRAND_MAP[key]
|
|
132
|
+
log.debug("Brand resolved (case-insensitive): #{brand_name} → #{result}")
|
|
133
|
+
return result
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Failed
|
|
137
|
+
log.error("Brand not found: #{brand_name}")
|
|
138
|
+
log.error("Available brands: #{BRAND_MAP.keys.join(', ')}")
|
|
139
|
+
raise BrandNotFoundError, "Unknown brand: #{brand_name}. Available: #{BRAND_MAP.keys.join(', ')}"
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
2. **Path construction:**
|
|
144
|
+
```ruby
|
|
145
|
+
# ❌ BAD: Will blow up if nil with no context
|
|
146
|
+
def project_path(brand, project_name)
|
|
147
|
+
File.join(video_projects_root, brand_folder(brand), project_name)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ✅ GOOD: Validate inputs and log construction
|
|
151
|
+
def project_path(brand, project_name)
|
|
152
|
+
log.debug("Constructing project path: brand=#{brand}, project=#{project_name}")
|
|
153
|
+
|
|
154
|
+
root = video_projects_root
|
|
155
|
+
if root.nil?
|
|
156
|
+
log.error("video_projects_root is nil - check settings.json")
|
|
157
|
+
raise ConfigurationError, "Missing video-projects-root configuration"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
folder = brand_folder(brand)
|
|
161
|
+
if folder.nil?
|
|
162
|
+
log.error("brand_folder returned nil for brand: #{brand}")
|
|
163
|
+
raise BrandError, "Could not resolve brand folder for: #{brand}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
path = File.join(root, folder, project_name)
|
|
167
|
+
log.debug("Project path constructed: #{path}")
|
|
168
|
+
path
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
3. **File/directory validation:**
|
|
173
|
+
```ruby
|
|
174
|
+
# ❌ BAD: Assumes path exists
|
|
175
|
+
def read_project_file(path)
|
|
176
|
+
File.read(path)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# ✅ GOOD: Validate and log
|
|
180
|
+
def read_project_file(path)
|
|
181
|
+
log.debug("Reading project file: #{path}")
|
|
182
|
+
|
|
183
|
+
unless File.exist?(path)
|
|
184
|
+
log.error("File not found: #{path}")
|
|
185
|
+
log.error("Parent directory exists: #{File.directory?(File.dirname(path))}")
|
|
186
|
+
log.error("Current directory: #{Dir.pwd}")
|
|
187
|
+
raise FileNotFoundError, "Project file not found: #{path}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
content = File.read(path)
|
|
191
|
+
log.debug("File read successfully: #{content.bytesize} bytes")
|
|
192
|
+
content
|
|
193
|
+
rescue Errno::EACCES => e
|
|
194
|
+
log.error("Permission denied reading file: #{path}")
|
|
195
|
+
log.error("File permissions: #{File.stat(path).mode.to_s(8)}")
|
|
196
|
+
raise PermissionError, "Cannot read #{path}: #{e.message}"
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Phase 3: API/External Service Calls
|
|
201
|
+
|
|
202
|
+
**Focus areas:**
|
|
203
|
+
- YouTube API calls
|
|
204
|
+
- OpenAI API calls
|
|
205
|
+
- AWS S3 operations
|
|
206
|
+
- File system operations
|
|
207
|
+
|
|
208
|
+
**What to check:**
|
|
209
|
+
|
|
210
|
+
1. **API call logging:**
|
|
211
|
+
```ruby
|
|
212
|
+
# ❌ BAD: No visibility into what's happening
|
|
213
|
+
def update_video(video_id, title)
|
|
214
|
+
youtube_service.update_video(video_id, title: title)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# ✅ GOOD: Log request and response
|
|
218
|
+
def update_video(video_id, title)
|
|
219
|
+
log.debug("YouTube API: Updating video #{video_id}")
|
|
220
|
+
log.debug("New title: #{title}")
|
|
221
|
+
|
|
222
|
+
response = youtube_service.update_video(video_id, title: title)
|
|
223
|
+
|
|
224
|
+
log.debug("YouTube API: Update successful")
|
|
225
|
+
log.debug("Response: #{response.inspect}")
|
|
226
|
+
response
|
|
227
|
+
rescue Google::Apis::ClientError => e
|
|
228
|
+
log.error("YouTube API error updating video #{video_id}")
|
|
229
|
+
log.error("Error code: #{e.status_code}")
|
|
230
|
+
log.error("Error message: #{e.message}")
|
|
231
|
+
log.error("Title attempted: #{title}")
|
|
232
|
+
raise
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
2. **Retry logic logging:**
|
|
237
|
+
```ruby
|
|
238
|
+
# ✅ GOOD: Log retry attempts
|
|
239
|
+
def api_call_with_retry(max_retries: 3)
|
|
240
|
+
retries = 0
|
|
241
|
+
begin
|
|
242
|
+
log.debug("API call attempt #{retries + 1}/#{max_retries}")
|
|
243
|
+
yield
|
|
244
|
+
rescue TransientError => e
|
|
245
|
+
retries += 1
|
|
246
|
+
if retries < max_retries
|
|
247
|
+
wait_time = 2 ** retries
|
|
248
|
+
log.warn("API call failed, retrying in #{wait_time}s (attempt #{retries}/#{max_retries})")
|
|
249
|
+
log.warn("Error: #{e.message}")
|
|
250
|
+
sleep wait_time
|
|
251
|
+
retry
|
|
252
|
+
else
|
|
253
|
+
log.error("API call failed after #{max_retries} attempts")
|
|
254
|
+
log.error("Final error: #{e.message}")
|
|
255
|
+
raise
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Phase 4: Type Coercion/Validation
|
|
262
|
+
|
|
263
|
+
**Focus areas:**
|
|
264
|
+
- Input validation in CLI commands
|
|
265
|
+
- Type conversion (string to int, etc.)
|
|
266
|
+
- Hash/array access with potential nil values
|
|
267
|
+
|
|
268
|
+
**What to check:**
|
|
269
|
+
|
|
270
|
+
1. **Type validation:**
|
|
271
|
+
```ruby
|
|
272
|
+
# ❌ BAD: Assumes type is correct
|
|
273
|
+
def process_limit(limit)
|
|
274
|
+
(0...limit).each { |i| process_item(i) }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ✅ GOOD: Validate and convert with logging
|
|
278
|
+
def process_limit(limit)
|
|
279
|
+
log.debug("Processing limit: #{limit.inspect} (#{limit.class})")
|
|
280
|
+
|
|
281
|
+
unless limit.respond_to?(:to_i)
|
|
282
|
+
log.error("Invalid limit type: #{limit.class}")
|
|
283
|
+
log.error("Expected: Integer or String, got: #{limit.inspect}")
|
|
284
|
+
raise ArgumentError, "Limit must be numeric, got: #{limit.class}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
limit_int = limit.to_i
|
|
288
|
+
if limit_int <= 0
|
|
289
|
+
log.warn("Non-positive limit: #{limit_int}, defaulting to 10")
|
|
290
|
+
limit_int = 10
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
log.debug("Processing #{limit_int} items")
|
|
294
|
+
(0...limit_int).each { |i| process_item(i) }
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
2. **Hash access safety:**
|
|
299
|
+
```ruby
|
|
300
|
+
# ❌ BAD: Will blow up if key missing
|
|
301
|
+
def get_channel_name(channel_data)
|
|
302
|
+
channel_data['name'].upcase
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# ✅ GOOD: Safe access with logging
|
|
306
|
+
def get_channel_name(channel_data)
|
|
307
|
+
log.debug("Extracting channel name from: #{channel_data.keys.join(', ')}")
|
|
308
|
+
|
|
309
|
+
name = channel_data['name']
|
|
310
|
+
if name.nil?
|
|
311
|
+
log.error("Missing 'name' key in channel data")
|
|
312
|
+
log.error("Available keys: #{channel_data.keys.join(', ')}")
|
|
313
|
+
log.error("Channel data: #{channel_data.inspect}")
|
|
314
|
+
raise DataError, "Channel data missing 'name' field"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
log.debug("Channel name: #{name}")
|
|
318
|
+
name.upcase
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Phase 5: Environment-Triggered Logging
|
|
323
|
+
|
|
324
|
+
**What to check:**
|
|
325
|
+
|
|
326
|
+
1. **Debug flag support:**
|
|
327
|
+
```ruby
|
|
328
|
+
# ✅ GOOD: Support for DEBUG environment variable
|
|
329
|
+
class Logger
|
|
330
|
+
def self.debug?
|
|
331
|
+
ENV['DEBUG'] == 'true' || ENV['DAM_DEBUG'] == 'true'
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def debug(message)
|
|
335
|
+
return unless self.class.debug?
|
|
336
|
+
puts "[DEBUG] #{Time.now.strftime('%H:%M:%S')} #{message}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def info(message)
|
|
340
|
+
puts "[INFO] #{message}"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def warn(message)
|
|
344
|
+
warn "[WARN] #{message}"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def error(message)
|
|
348
|
+
warn "[ERROR] #{message}"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
2. **Usage instructions:**
|
|
354
|
+
```bash
|
|
355
|
+
# Normal operation (quiet)
|
|
356
|
+
dam list appydave
|
|
357
|
+
|
|
358
|
+
# Debug mode (verbose)
|
|
359
|
+
DEBUG=true dam list appydave
|
|
360
|
+
DAM_DEBUG=true dam list appydave
|
|
361
|
+
|
|
362
|
+
# Debug mode for specific command
|
|
363
|
+
DEBUG=true gpt_context -i '**/*.rb' -d
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
3. **Documentation:**
|
|
367
|
+
```markdown
|
|
368
|
+
## Debugging
|
|
369
|
+
|
|
370
|
+
Enable debug logging with environment variables:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
DEBUG=true dam list appydave
|
|
374
|
+
DAM_DEBUG=true vat s3-status voz boy-baker
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Debug logs show:
|
|
378
|
+
- Configuration loading and values
|
|
379
|
+
- Path resolution steps
|
|
380
|
+
- API request/response details
|
|
381
|
+
- File operations and validations
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Phase 6: Silent Failure Detection
|
|
385
|
+
|
|
386
|
+
**What to flag:**
|
|
387
|
+
|
|
388
|
+
1. **Rescue without logging:**
|
|
389
|
+
```ruby
|
|
390
|
+
# ❌ BAD: Silent failure
|
|
391
|
+
def safe_operation
|
|
392
|
+
risky_operation
|
|
393
|
+
rescue => e
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# ✅ GOOD: Logged failure
|
|
398
|
+
def safe_operation
|
|
399
|
+
risky_operation
|
|
400
|
+
rescue => e
|
|
401
|
+
log.error("Operation failed: #{e.class}")
|
|
402
|
+
log.error("Error message: #{e.message}")
|
|
403
|
+
log.error("Backtrace: #{e.backtrace.first(5).join("\n")}")
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
2. **Empty rescue blocks:**
|
|
409
|
+
```ruby
|
|
410
|
+
# ❌ BAD: Swallows all errors
|
|
411
|
+
begin
|
|
412
|
+
operation
|
|
413
|
+
rescue
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# ✅ GOOD: At minimum, log it
|
|
417
|
+
begin
|
|
418
|
+
operation
|
|
419
|
+
rescue => e
|
|
420
|
+
log.warn("Operation failed but continuing: #{e.message}")
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
3. **Unhelpful error messages:**
|
|
425
|
+
```ruby
|
|
426
|
+
# ❌ BAD: Generic error
|
|
427
|
+
raise "Invalid input"
|
|
428
|
+
|
|
429
|
+
# ✅ GOOD: Specific error with context
|
|
430
|
+
raise ArgumentError, "Invalid brand name: #{brand}. Expected one of: #{VALID_BRANDS.join(', ')}"
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Report Format
|
|
434
|
+
|
|
435
|
+
```markdown
|
|
436
|
+
# Defensive Logging Audit - [Date]
|
|
437
|
+
|
|
438
|
+
## Summary
|
|
439
|
+
- **Files analyzed:** X Ruby files in lib/
|
|
440
|
+
- **Critical gaps:** Y locations missing nil guards
|
|
441
|
+
- **Silent failures:** Z rescue blocks without logging
|
|
442
|
+
- **Good patterns found:** N files with proper logging
|
|
443
|
+
|
|
444
|
+
## Critical Gaps 🔴
|
|
445
|
+
|
|
446
|
+
### 1. Configuration Loading - Missing Nil Guards
|
|
447
|
+
**Location:** `lib/appydave/tools/configuration/settings.rb:45`
|
|
448
|
+
|
|
449
|
+
**Current code:**
|
|
450
|
+
```ruby
|
|
451
|
+
def video_projects_root
|
|
452
|
+
config['video-projects-root']
|
|
453
|
+
end
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Problem:**
|
|
457
|
+
- Returns nil if key missing
|
|
458
|
+
- Downstream code will fail with opaque "undefined method for nil:NilClass"
|
|
459
|
+
- No indication of what configuration is missing
|
|
460
|
+
|
|
461
|
+
**Recommended fix:**
|
|
462
|
+
```ruby
|
|
463
|
+
def video_projects_root
|
|
464
|
+
value = config['video-projects-root']
|
|
465
|
+
if value.nil?
|
|
466
|
+
log.error("Missing 'video-projects-root' in settings.json")
|
|
467
|
+
log.error("Config file: #{config_path}")
|
|
468
|
+
log.error("Available keys: #{config.keys.join(', ')}")
|
|
469
|
+
raise ConfigurationError, "Missing required setting 'video-projects-root'. Run 'ad_config -e' to configure."
|
|
470
|
+
end
|
|
471
|
+
log.debug("video-projects-root: #{value}")
|
|
472
|
+
value
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Impact:** High - This is the "Jan Problem" - remote users can't debug config issues
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### 2. Silent API Failures
|
|
481
|
+
**Location:** `lib/appydave/tools/youtube_manager/client.rb:89`
|
|
482
|
+
|
|
483
|
+
**Current code:**
|
|
484
|
+
```ruby
|
|
485
|
+
def update_video(video_id, title)
|
|
486
|
+
youtube_service.update_video(video_id, title: title)
|
|
487
|
+
rescue => e
|
|
488
|
+
nil
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Problem:**
|
|
493
|
+
- API failures are silent
|
|
494
|
+
- No logging of what went wrong
|
|
495
|
+
- Returns nil without explanation
|
|
496
|
+
|
|
497
|
+
**Recommended fix:**
|
|
498
|
+
```ruby
|
|
499
|
+
def update_video(video_id, title)
|
|
500
|
+
log.debug("YouTube API: Updating video #{video_id}")
|
|
501
|
+
log.debug("New title: #{title}")
|
|
502
|
+
|
|
503
|
+
result = youtube_service.update_video(video_id, title: title)
|
|
504
|
+
log.debug("Update successful")
|
|
505
|
+
result
|
|
506
|
+
rescue Google::Apis::ClientError => e
|
|
507
|
+
log.error("YouTube API error: #{e.status_code}")
|
|
508
|
+
log.error("Message: #{e.message}")
|
|
509
|
+
log.error("Video ID: #{video_id}")
|
|
510
|
+
log.error("Title: #{title}")
|
|
511
|
+
raise
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Impact:** High - API failures are hard to diagnose
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Moderate Gaps 🟡
|
|
520
|
+
|
|
521
|
+
### 1. Path Validation Missing
|
|
522
|
+
**Locations:**
|
|
523
|
+
- `lib/appydave/tools/dam/project_resolver.rb:34`
|
|
524
|
+
- `lib/appydave/tools/dam/commands/list.rb:56`
|
|
525
|
+
|
|
526
|
+
**Issue:** Path construction doesn't validate that directories exist
|
|
527
|
+
|
|
528
|
+
**Recommendation:**
|
|
529
|
+
```ruby
|
|
530
|
+
def validate_path(path, description)
|
|
531
|
+
log.debug("Validating #{description}: #{path}")
|
|
532
|
+
|
|
533
|
+
unless File.exist?(path)
|
|
534
|
+
log.error("#{description} not found: #{path}")
|
|
535
|
+
log.error("Parent exists: #{File.directory?(File.dirname(path))}")
|
|
536
|
+
raise PathError, "#{description} does not exist: #{path}"
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
log.debug("#{description} validated")
|
|
540
|
+
path
|
|
541
|
+
end
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
### 2. No Debug Mode Support
|
|
547
|
+
**Issue:** No consistent way to enable debug logging
|
|
548
|
+
|
|
549
|
+
**Recommendation:**
|
|
550
|
+
1. Add Logger class with debug flag support
|
|
551
|
+
2. Document DEBUG=true usage
|
|
552
|
+
3. Add to all CLI commands
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## Good Patterns Found ✅
|
|
557
|
+
|
|
558
|
+
### 1. Configuration Echo in DAM
|
|
559
|
+
**Location:** `lib/appydave/tools/dam/configuration.rb:23-30`
|
|
560
|
+
|
|
561
|
+
**Code:**
|
|
562
|
+
```ruby
|
|
563
|
+
def log_configuration
|
|
564
|
+
return unless debug?
|
|
565
|
+
puts "=== DAM Configuration ==="
|
|
566
|
+
puts "Video projects root: #{video_projects_root}"
|
|
567
|
+
puts "Current directory: #{Dir.pwd}"
|
|
568
|
+
puts "========================="
|
|
569
|
+
end
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Why it's good:** Helps users verify configuration is loaded correctly
|
|
573
|
+
|
|
574
|
+
**Recommend:** Apply this pattern to all configuration loading
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### 2. Detailed S3 Error Logging
|
|
579
|
+
**Location:** `lib/appydave/tools/dam/s3_sync.rb:78-85`
|
|
580
|
+
|
|
581
|
+
**Why it's good:** S3 errors include bucket, key, and AWS error details
|
|
582
|
+
|
|
583
|
+
**Recommend:** Apply this pattern to all external service calls
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Implementation Priority
|
|
588
|
+
|
|
589
|
+
### Phase 1: Critical Configuration Issues (Do Now)
|
|
590
|
+
1. [ ] Add nil guards to all configuration value access
|
|
591
|
+
2. [ ] Add configuration echo on CLI startup (when DEBUG=true)
|
|
592
|
+
3. [ ] Add helpful error messages for missing config values
|
|
593
|
+
|
|
594
|
+
**Files to update:**
|
|
595
|
+
- `lib/appydave/tools/configuration/settings.rb`
|
|
596
|
+
- `lib/appydave/tools/configuration/channels.rb`
|
|
597
|
+
- `lib/appydave/tools/dam/configuration.rb`
|
|
598
|
+
|
|
599
|
+
**Estimated effort:** 2-3 hours
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
### Phase 2: Path Resolution Safety (Do Soon)
|
|
604
|
+
1. [ ] Add logging to brand resolution
|
|
605
|
+
2. [ ] Add path validation with existence checks
|
|
606
|
+
3. [ ] Add detailed error messages for path failures
|
|
607
|
+
|
|
608
|
+
**Files to update:**
|
|
609
|
+
- `lib/appydave/tools/dam/project_resolver.rb`
|
|
610
|
+
- `lib/appydave/tools/dam/brand_resolver.rb`
|
|
611
|
+
|
|
612
|
+
**Estimated effort:** 1-2 hours
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### Phase 3: API Call Logging (Do When Touching API Code)
|
|
617
|
+
1. [ ] Add request/response logging to YouTube API
|
|
618
|
+
2. [ ] Add retry logging
|
|
619
|
+
3. [ ] Add error context to API failures
|
|
620
|
+
|
|
621
|
+
**Files to update:**
|
|
622
|
+
- `lib/appydave/tools/youtube_manager/client.rb`
|
|
623
|
+
- `lib/appydave/tools/cli_actions/youtube_*.rb`
|
|
624
|
+
|
|
625
|
+
**Estimated effort:** 1-2 hours
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
### Phase 4: Debug Mode Infrastructure (Nice to Have)
|
|
630
|
+
1. [ ] Create consistent Logger class
|
|
631
|
+
2. [ ] Add DEBUG environment variable support
|
|
632
|
+
3. [ ] Document debug mode in README
|
|
633
|
+
|
|
634
|
+
**New files:**
|
|
635
|
+
- `lib/appydave/tools/utils/logger.rb`
|
|
636
|
+
|
|
637
|
+
**Estimated effort:** 1 hour
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Testing Recommendations
|
|
642
|
+
|
|
643
|
+
### Manual Testing Checklist
|
|
644
|
+
```bash
|
|
645
|
+
# Test missing configuration
|
|
646
|
+
rm ~/.config/appydave/settings.json
|
|
647
|
+
dam list appydave
|
|
648
|
+
# Expected: Clear error message about missing settings.json
|
|
649
|
+
|
|
650
|
+
# Test missing configuration value
|
|
651
|
+
echo '{}' > ~/.config/appydave/settings.json
|
|
652
|
+
dam list appydave
|
|
653
|
+
# Expected: Clear error message about missing video-projects-root
|
|
654
|
+
|
|
655
|
+
# Test debug mode
|
|
656
|
+
DEBUG=true dam list appydave
|
|
657
|
+
# Expected: Verbose logging of configuration loading, path resolution
|
|
658
|
+
|
|
659
|
+
# Test invalid brand
|
|
660
|
+
dam list invalid-brand
|
|
661
|
+
# Expected: Error message listing valid brands
|
|
662
|
+
|
|
663
|
+
# Test missing project directory
|
|
664
|
+
echo '{"video-projects-root": "/tmp/does-not-exist"}' > ~/.config/appydave/settings.json
|
|
665
|
+
dam list appydave
|
|
666
|
+
# Expected: Clear error about directory not existing
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Automated Testing
|
|
670
|
+
Add specs that verify error messages:
|
|
671
|
+
|
|
672
|
+
```ruby
|
|
673
|
+
RSpec.describe Configuration do
|
|
674
|
+
context 'when configuration is missing' do
|
|
675
|
+
it 'raises helpful error' do
|
|
676
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
677
|
+
|
|
678
|
+
expect { config.video_projects_root }.to raise_error(
|
|
679
|
+
ConfigurationError,
|
|
680
|
+
/Missing required setting 'video-projects-root'/
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## Logging Standards for This Project
|
|
690
|
+
|
|
691
|
+
### Log Levels
|
|
692
|
+
- **debug:** Detailed flow information (config values, paths, API requests)
|
|
693
|
+
- **info:** High-level operations (command started, operation completed)
|
|
694
|
+
- **warn:** Recoverable issues (using defaults, retrying operations)
|
|
695
|
+
- **error:** Failures that prevent operation (missing config, API errors)
|
|
696
|
+
|
|
697
|
+
### Log Format
|
|
698
|
+
```ruby
|
|
699
|
+
log.debug("#{self.class.name}##{__method__}: #{message}")
|
|
700
|
+
log.error("ERROR in #{self.class.name}##{__method__}: #{message}")
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### What to Log
|
|
704
|
+
✅ **Always log:**
|
|
705
|
+
- Configuration loading (in debug mode)
|
|
706
|
+
- Configuration value access (in debug mode)
|
|
707
|
+
- Path construction and validation (in debug mode)
|
|
708
|
+
- External API calls (request and response)
|
|
709
|
+
- File operations (read, write, delete)
|
|
710
|
+
- Error conditions (always, not just debug mode)
|
|
711
|
+
|
|
712
|
+
❌ **Don't log:**
|
|
713
|
+
- Secrets (API keys, tokens, passwords)
|
|
714
|
+
- User personal data (unless absolutely necessary for debugging)
|
|
715
|
+
- Inside tight loops (will spam logs)
|
|
716
|
+
|
|
717
|
+
### Debug Mode Usage
|
|
718
|
+
```ruby
|
|
719
|
+
# Enable via environment variable
|
|
720
|
+
DEBUG=true dam list appydave
|
|
721
|
+
|
|
722
|
+
# Or project-specific flag
|
|
723
|
+
DAM_DEBUG=true dam s3-status voz boy-baker
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## Example: Before and After
|
|
729
|
+
|
|
730
|
+
### Before (Current Code)
|
|
731
|
+
```ruby
|
|
732
|
+
def project_path
|
|
733
|
+
root = config['video-projects-root']
|
|
734
|
+
brand = brand_folder
|
|
735
|
+
File.join(root, brand, project_name)
|
|
736
|
+
end
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Problems:**
|
|
740
|
+
- No logging of what's happening
|
|
741
|
+
- If `root` is nil, error is "undefined method `join' for nil:NilClass" (useless)
|
|
742
|
+
- If `brand` is nil, error is equally useless
|
|
743
|
+
- No way to debug where the nil came from
|
|
744
|
+
|
|
745
|
+
### After (With Defensive Logging)
|
|
746
|
+
```ruby
|
|
747
|
+
def project_path
|
|
748
|
+
log.debug("#{self.class}#project_path called")
|
|
749
|
+
log.debug(" project_name: #{project_name}")
|
|
750
|
+
|
|
751
|
+
root = config['video-projects-root']
|
|
752
|
+
if root.nil?
|
|
753
|
+
log.error("Configuration missing: video-projects-root")
|
|
754
|
+
log.error(" Config file: #{config_path}")
|
|
755
|
+
log.error(" Available keys: #{config.keys.join(', ')}")
|
|
756
|
+
raise ConfigurationError, "Missing 'video-projects-root' in settings.json. Run 'ad_config -e' to set."
|
|
757
|
+
end
|
|
758
|
+
log.debug(" video-projects-root: #{root}")
|
|
759
|
+
|
|
760
|
+
brand = brand_folder
|
|
761
|
+
if brand.nil?
|
|
762
|
+
log.error("Brand resolution failed for: #{@brand_name}")
|
|
763
|
+
log.error(" Available brands: #{BrandResolver::BRAND_MAP.keys.join(', ')}")
|
|
764
|
+
raise BrandError, "Could not resolve brand: #{@brand_name}"
|
|
765
|
+
end
|
|
766
|
+
log.debug(" brand_folder: #{brand}")
|
|
767
|
+
|
|
768
|
+
path = File.join(root, brand, project_name)
|
|
769
|
+
log.debug(" final path: #{path}")
|
|
770
|
+
|
|
771
|
+
unless File.directory?(path)
|
|
772
|
+
log.warn("Project directory does not exist: #{path}")
|
|
773
|
+
log.warn(" This may be a new project or misconfigured path")
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
path
|
|
777
|
+
end
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**Benefits:**
|
|
781
|
+
- Every step is logged (in debug mode)
|
|
782
|
+
- Nil values are caught with context
|
|
783
|
+
- Error messages tell you exactly what's missing
|
|
784
|
+
- User gets actionable instructions ("Run 'ad_config -e'")
|
|
785
|
+
- Remote debugging is actually possible
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Notes for AI Assistants
|
|
790
|
+
|
|
791
|
+
- **This is a safety audit, not a refactoring** - Focus on finding gaps, not rewriting code
|
|
792
|
+
- **Nil errors are the enemy** - Every place that could return nil should be logged
|
|
793
|
+
- **Configuration is critical** - This is the #1 source of remote debugging pain
|
|
794
|
+
- **Be specific** - Show exact file locations and code snippets
|
|
795
|
+
- **Provide fixes** - Don't just identify problems, show how to fix them
|
|
796
|
+
- **Consider the user** - Error messages should help users fix their config, not just report failures
|
|
797
|
+
- **Debug mode is key** - Users should be able to turn on verbose logging when needed
|
|
798
|
+
- **Don't break existing code** - Additions only, don't change working logic
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
**Last updated:** 2025-01-21
|