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.
@@ -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