appydave-tools 0.37.0 → 0.38.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 +13 -0
- data/docs/code-quality/README.md +177 -0
- data/docs/code-quality/implementation-plan.md +712 -0
- data/docs/code-quality/report-2025-01-21.md +342 -0
- data/lib/appydave/tools/dam/errors.rb +31 -0
- data/lib/appydave/tools/dam/file_helper.rb +43 -0
- data/lib/appydave/tools/dam/git_helper.rb +89 -0
- data/lib/appydave/tools/dam/project_listing.rb +4 -25
- data/lib/appydave/tools/dam/repo_push.rb +2 -7
- data/lib/appydave/tools/dam/repo_status.rb +7 -24
- data/lib/appydave/tools/dam/s3_operations.rb +1 -5
- data/lib/appydave/tools/dam/status.rb +6 -19
- data/lib/appydave/tools/dam/sync_from_ssd.rb +1 -7
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +3 -0
- data/package.json +1 -1
- metadata +7 -1
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
# Code Quality Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Based on:** [report-2025-01-21.md](./report-2025-01-21.md)
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This document outlines the step-by-step implementation plan for addressing code quality issues identified in the Jan 21, 2025 retrospective analysis.
|
|
8
|
+
|
|
9
|
+
## Implementation Strategy
|
|
10
|
+
|
|
11
|
+
### Phase 1: Quick Wins (1-2 days)
|
|
12
|
+
Extract duplicated utility code and establish patterns.
|
|
13
|
+
|
|
14
|
+
### Phase 2: Architectural Improvements (2-3 days)
|
|
15
|
+
Address core architectural issues (brand resolution, git helpers).
|
|
16
|
+
|
|
17
|
+
### Phase 3: Test Improvements (1 day)
|
|
18
|
+
Refactor tests to use consistent patterns.
|
|
19
|
+
|
|
20
|
+
### Phase 4: Documentation & Cleanup (1 day)
|
|
21
|
+
Document patterns and create guidelines.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Phase 1: Quick Wins (Priority 1)
|
|
26
|
+
|
|
27
|
+
### Task 1.1: Extract FileUtils Module
|
|
28
|
+
**Effort:** 1 hour | **Priority:** Medium | **Risk:** Low
|
|
29
|
+
|
|
30
|
+
**Goal:** Consolidate directory size calculation into single module.
|
|
31
|
+
|
|
32
|
+
**Steps:**
|
|
33
|
+
1. Create `lib/appydave/tools/dam/file_utils.rb`
|
|
34
|
+
2. Extract methods:
|
|
35
|
+
- `calculate_directory_size(path)` - from project_listing.rb:154, s3_operations.rb:770
|
|
36
|
+
- `format_size(bytes)` - from project_listing.rb:176, sync_from_ssd.rb:270
|
|
37
|
+
3. Update callers:
|
|
38
|
+
- `lib/appydave/tools/dam/project_listing.rb` (2 locations)
|
|
39
|
+
- `lib/appydave/tools/dam/s3_operations.rb` (1 location)
|
|
40
|
+
- `lib/appydave/tools/dam/sync_from_ssd.rb` (1 location)
|
|
41
|
+
4. Write specs: `spec/appydave/tools/dam/file_utils_spec.rb`
|
|
42
|
+
5. Run tests: `bundle exec rspec spec/appydave/tools/dam/`
|
|
43
|
+
|
|
44
|
+
**Implementation Example:**
|
|
45
|
+
```ruby
|
|
46
|
+
# lib/appydave/tools/dam/file_utils.rb
|
|
47
|
+
module Appydave
|
|
48
|
+
module Tools
|
|
49
|
+
module Dam
|
|
50
|
+
module FileUtils
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# Calculate total size of directory in bytes
|
|
54
|
+
# @param path [String] Directory path
|
|
55
|
+
# @return [Integer] Size in bytes
|
|
56
|
+
def calculate_directory_size(path)
|
|
57
|
+
return 0 unless Dir.exist?(path)
|
|
58
|
+
|
|
59
|
+
total = 0
|
|
60
|
+
Find.find(path) do |file_path|
|
|
61
|
+
total += File.size(file_path) if File.file?(file_path)
|
|
62
|
+
rescue StandardError
|
|
63
|
+
# Skip files we can't read
|
|
64
|
+
end
|
|
65
|
+
total
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Format bytes into human-readable size
|
|
69
|
+
# @param bytes [Integer] Size in bytes
|
|
70
|
+
# @return [String] Formatted size (e.g., "1.5 GB")
|
|
71
|
+
def format_size(bytes)
|
|
72
|
+
return '0 B' if bytes.zero?
|
|
73
|
+
|
|
74
|
+
units = %w[B KB MB GB TB]
|
|
75
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
76
|
+
exp = [exp, units.length - 1].min
|
|
77
|
+
|
|
78
|
+
format('%.1f %s', bytes.to_f / (1024**exp), units[exp])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Testing Strategy:**
|
|
87
|
+
```ruby
|
|
88
|
+
RSpec.describe Appydave::Tools::Dam::FileUtils do
|
|
89
|
+
describe '.calculate_directory_size' do
|
|
90
|
+
it 'returns 0 for non-existent directory'
|
|
91
|
+
it 'calculates size of directory with files'
|
|
92
|
+
it 'handles permission errors gracefully'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe '.format_size' do
|
|
96
|
+
it 'formats bytes correctly' do
|
|
97
|
+
expect(described_class.format_size(0)).to eq('0 B')
|
|
98
|
+
expect(described_class.format_size(1024)).to eq('1.0 KB')
|
|
99
|
+
expect(described_class.format_size(1_048_576)).to eq('1.0 MB')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Success Criteria:**
|
|
106
|
+
- [ ] All tests pass
|
|
107
|
+
- [ ] 4 files updated to use new module
|
|
108
|
+
- [ ] ~40 lines of duplication eliminated
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### Task 1.2: Define DAM Exception Hierarchy
|
|
113
|
+
**Effort:** 2 hours | **Priority:** Medium | **Risk:** Low
|
|
114
|
+
|
|
115
|
+
**Goal:** Create consistent error handling pattern for DAM module.
|
|
116
|
+
|
|
117
|
+
**Steps:**
|
|
118
|
+
1. Create `lib/appydave/tools/dam/errors.rb`
|
|
119
|
+
2. Define exception classes
|
|
120
|
+
3. Update error raises in:
|
|
121
|
+
- `project_resolver.rb:19` (string raise → ProjectNotFoundError)
|
|
122
|
+
- `config.rb:40` (string raise → BrandNotFoundError)
|
|
123
|
+
- Other locations using string raises
|
|
124
|
+
4. Write specs
|
|
125
|
+
5. Update CLAUDE.md with error handling guidelines
|
|
126
|
+
|
|
127
|
+
**Implementation:**
|
|
128
|
+
```ruby
|
|
129
|
+
# lib/appydave/tools/dam/errors.rb
|
|
130
|
+
module Appydave
|
|
131
|
+
module Tools
|
|
132
|
+
module Dam
|
|
133
|
+
# Base error for all DAM operations
|
|
134
|
+
class DamError < StandardError; end
|
|
135
|
+
|
|
136
|
+
# Raised when brand directory not found
|
|
137
|
+
class BrandNotFoundError < DamError
|
|
138
|
+
def initialize(brand, available_brands = [])
|
|
139
|
+
message = "Brand directory not found: #{brand}"
|
|
140
|
+
if available_brands.any?
|
|
141
|
+
message += "\n\nAvailable brands:\n#{available_brands.join("\n")}"
|
|
142
|
+
end
|
|
143
|
+
super(message)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Raised when project not found in brand
|
|
148
|
+
class ProjectNotFoundError < DamError; end
|
|
149
|
+
|
|
150
|
+
# Raised when configuration invalid or missing
|
|
151
|
+
class ConfigurationError < DamError; end
|
|
152
|
+
|
|
153
|
+
# Raised when S3 operation fails
|
|
154
|
+
class S3OperationError < DamError; end
|
|
155
|
+
|
|
156
|
+
# Raised when git operation fails
|
|
157
|
+
class GitOperationError < DamError; end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Migration Example:**
|
|
164
|
+
```ruby
|
|
165
|
+
# BEFORE (project_resolver.rb:19)
|
|
166
|
+
raise '❌ Project name is required' if project_hint.nil? || project_hint.empty?
|
|
167
|
+
|
|
168
|
+
# AFTER
|
|
169
|
+
raise ProjectNotFoundError, 'Project name is required' if project_hint.nil? || project_hint.empty?
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Success Criteria:**
|
|
173
|
+
- [ ] All string raises replaced with typed exceptions
|
|
174
|
+
- [ ] CLI error handling in bin/dam still works
|
|
175
|
+
- [ ] Error messages remain user-friendly
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Phase 2: Architectural Improvements (Priority 2)
|
|
180
|
+
|
|
181
|
+
### Task 2.1: Extract GitHelper Module
|
|
182
|
+
**Effort:** 3 hours | **Priority:** High | **Risk:** Medium
|
|
183
|
+
|
|
184
|
+
**Goal:** Eliminate 90 lines of git command duplication.
|
|
185
|
+
|
|
186
|
+
**Steps:**
|
|
187
|
+
1. Create `lib/appydave/tools/dam/git_helper.rb`
|
|
188
|
+
2. Extract git methods from:
|
|
189
|
+
- `status.rb:243-275` (4 methods)
|
|
190
|
+
- `repo_status.rb:109-144` (6 methods)
|
|
191
|
+
- `repo_push.rb:107-119` (3 methods)
|
|
192
|
+
3. Make module methods accept `repo_path` parameter
|
|
193
|
+
4. Include module in classes
|
|
194
|
+
5. Update all callers
|
|
195
|
+
6. Write comprehensive specs
|
|
196
|
+
7. Run full test suite
|
|
197
|
+
|
|
198
|
+
**Implementation:**
|
|
199
|
+
```ruby
|
|
200
|
+
# lib/appydave/tools/dam/git_helper.rb
|
|
201
|
+
module Appydave
|
|
202
|
+
module Tools
|
|
203
|
+
module Dam
|
|
204
|
+
# Git operations helper for DAM classes
|
|
205
|
+
# Provides reusable git command wrappers
|
|
206
|
+
module GitHelper
|
|
207
|
+
module_function
|
|
208
|
+
|
|
209
|
+
# Get current branch name
|
|
210
|
+
# @param repo_path [String] Path to git repository
|
|
211
|
+
# @return [String] Branch name or 'unknown' if error
|
|
212
|
+
def current_branch(repo_path)
|
|
213
|
+
`git -C "#{repo_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
214
|
+
rescue StandardError
|
|
215
|
+
'unknown'
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Get git remote URL
|
|
219
|
+
# @param repo_path [String] Path to git repository
|
|
220
|
+
# @return [String, nil] Remote URL or nil if not configured
|
|
221
|
+
def remote_url(repo_path)
|
|
222
|
+
result = `git -C "#{repo_path}" remote get-url origin 2>/dev/null`.strip
|
|
223
|
+
result.empty? ? nil : result
|
|
224
|
+
rescue StandardError
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Count commits ahead of remote
|
|
229
|
+
# @param repo_path [String] Path to git repository
|
|
230
|
+
# @return [Integer] Number of commits ahead
|
|
231
|
+
def commits_ahead(repo_path)
|
|
232
|
+
`git -C "#{repo_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
|
|
233
|
+
rescue StandardError
|
|
234
|
+
0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Count commits behind remote
|
|
238
|
+
# @param repo_path [String] Path to git repository
|
|
239
|
+
# @return [Integer] Number of commits behind
|
|
240
|
+
def commits_behind(repo_path)
|
|
241
|
+
`git -C "#{repo_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
|
|
242
|
+
rescue StandardError
|
|
243
|
+
0
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Count modified files
|
|
247
|
+
# @param repo_path [String] Path to git repository
|
|
248
|
+
# @return [Integer] Number of modified files
|
|
249
|
+
def modified_files_count(repo_path)
|
|
250
|
+
`git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
|
|
251
|
+
rescue StandardError
|
|
252
|
+
0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Count untracked files
|
|
256
|
+
# @param repo_path [String] Path to git repository
|
|
257
|
+
# @return [Integer] Number of untracked files
|
|
258
|
+
def untracked_files_count(repo_path)
|
|
259
|
+
`git -C "#{repo_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
|
|
260
|
+
rescue StandardError
|
|
261
|
+
0
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Check if repository has uncommitted changes
|
|
265
|
+
# @param repo_path [String] Path to git repository
|
|
266
|
+
# @return [Boolean] true if changes exist
|
|
267
|
+
def uncommitted_changes?(repo_path)
|
|
268
|
+
system("git -C \"#{repo_path}\" diff-index --quiet HEAD -- 2>/dev/null")
|
|
269
|
+
!$CHILD_STATUS.success?
|
|
270
|
+
rescue StandardError
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Fetch from remote
|
|
275
|
+
# @param repo_path [String] Path to git repository
|
|
276
|
+
# @return [Boolean] true if successful
|
|
277
|
+
def fetch(repo_path)
|
|
278
|
+
system("git -C \"#{repo_path}\" fetch 2>/dev/null")
|
|
279
|
+
$CHILD_STATUS.success?
|
|
280
|
+
rescue StandardError
|
|
281
|
+
false
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Migration Example:**
|
|
290
|
+
```ruby
|
|
291
|
+
# BEFORE (status.rb)
|
|
292
|
+
class Status
|
|
293
|
+
def current_branch
|
|
294
|
+
`git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# AFTER (status.rb)
|
|
299
|
+
class Status
|
|
300
|
+
include GitHelper
|
|
301
|
+
|
|
302
|
+
def current_branch
|
|
303
|
+
GitHelper.current_branch(brand_path)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Testing Strategy:**
|
|
309
|
+
```ruby
|
|
310
|
+
RSpec.describe Appydave::Tools::Dam::GitHelper do
|
|
311
|
+
let(:temp_repo) { Dir.mktmpdir }
|
|
312
|
+
|
|
313
|
+
before do
|
|
314
|
+
# Initialize git repo for testing
|
|
315
|
+
system("git init #{temp_repo} 2>/dev/null")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
after { FileUtils.rm_rf(temp_repo) }
|
|
319
|
+
|
|
320
|
+
describe '.current_branch' do
|
|
321
|
+
it 'returns branch name'
|
|
322
|
+
it 'handles non-git directory gracefully'
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# ... similar for other methods
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Success Criteria:**
|
|
330
|
+
- [ ] All 3 classes using GitHelper
|
|
331
|
+
- [ ] All tests pass
|
|
332
|
+
- [ ] ~90 lines of duplication eliminated
|
|
333
|
+
- [ ] Git commands centralized
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### Task 2.2: Create BrandResolver Class
|
|
338
|
+
**Effort:** 6-8 hours | **Priority:** High | **Risk:** High
|
|
339
|
+
|
|
340
|
+
**Goal:** Centralize brand name transformation logic to prevent ongoing bugs.
|
|
341
|
+
|
|
342
|
+
**Steps:**
|
|
343
|
+
1. Create `lib/appydave/tools/dam/brand_resolver.rb`
|
|
344
|
+
2. Move logic from:
|
|
345
|
+
- `config.rb:91-116` (expand_brand)
|
|
346
|
+
- `project_resolver.rb:118-121` (strip v- prefix)
|
|
347
|
+
3. Define clear API:
|
|
348
|
+
- `expand(shortcut)` - appydave → v-appydave
|
|
349
|
+
- `normalize(brand)` - v-appydave → appydave
|
|
350
|
+
- `validate(brand)` - raises if invalid
|
|
351
|
+
- `to_config_key(brand)` - always key form (no v-)
|
|
352
|
+
- `to_display(brand)` - always display form (v-)
|
|
353
|
+
4. Update all callers (10+ files)
|
|
354
|
+
5. Write comprehensive specs
|
|
355
|
+
6. Document API in class comments
|
|
356
|
+
|
|
357
|
+
**Implementation:**
|
|
358
|
+
```ruby
|
|
359
|
+
# lib/appydave/tools/dam/brand_resolver.rb
|
|
360
|
+
module Appydave
|
|
361
|
+
module Tools
|
|
362
|
+
module Dam
|
|
363
|
+
# Centralized brand name resolution and transformation
|
|
364
|
+
#
|
|
365
|
+
# Handles conversion between:
|
|
366
|
+
# - Shortcuts: 'appydave', 'ad', 'joy', 'ss'
|
|
367
|
+
# - Config keys: 'appydave', 'beauty-and-joy', 'supportsignal'
|
|
368
|
+
# - Display names: 'v-appydave', 'v-beauty-and-joy', 'v-supportsignal'
|
|
369
|
+
#
|
|
370
|
+
# @example
|
|
371
|
+
# BrandResolver.expand('ad') # => 'v-appydave'
|
|
372
|
+
# BrandResolver.normalize('v-voz') # => 'voz'
|
|
373
|
+
# BrandResolver.to_config_key('ad') # => 'appydave'
|
|
374
|
+
# BrandResolver.to_display('voz') # => 'v-voz'
|
|
375
|
+
class BrandResolver
|
|
376
|
+
class << self
|
|
377
|
+
# Expand shortcut or key to full display name
|
|
378
|
+
# @param shortcut [String] Brand shortcut or key
|
|
379
|
+
# @return [String] Full brand name with v- prefix
|
|
380
|
+
def expand(shortcut)
|
|
381
|
+
return shortcut if shortcut.to_s.start_with?('v-')
|
|
382
|
+
|
|
383
|
+
key = to_config_key(shortcut)
|
|
384
|
+
"v-#{key}"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Normalize brand name to config key (strip v- prefix)
|
|
388
|
+
# @param brand [String] Brand name (with or without v-)
|
|
389
|
+
# @return [String] Config key without v- prefix
|
|
390
|
+
def normalize(brand)
|
|
391
|
+
brand.to_s.sub(/^v-/, '')
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Convert to config key (handles shortcuts)
|
|
395
|
+
# @param input [String] Shortcut, key, or display name
|
|
396
|
+
# @return [String] Config key
|
|
397
|
+
def to_config_key(input)
|
|
398
|
+
# Strip v- prefix first
|
|
399
|
+
normalized = normalize(input)
|
|
400
|
+
|
|
401
|
+
# Look up from brands.json
|
|
402
|
+
Appydave::Tools::Configuration::Config.configure
|
|
403
|
+
brands_config = Appydave::Tools::Configuration::Config.brands
|
|
404
|
+
|
|
405
|
+
# Check if matches brand key
|
|
406
|
+
brand = brands_config.brands.find { |b| b.key.downcase == normalized.downcase }
|
|
407
|
+
return brand.key if brand
|
|
408
|
+
|
|
409
|
+
# Check if matches shortcut
|
|
410
|
+
brand = brands_config.brands.find { |b| b.shortcut.downcase == normalized.downcase }
|
|
411
|
+
return brand.key if brand
|
|
412
|
+
|
|
413
|
+
# Fall back to hardcoded shortcuts (backward compatibility)
|
|
414
|
+
case normalized.downcase
|
|
415
|
+
when 'joy' then 'beauty-and-joy'
|
|
416
|
+
when 'ss' then 'supportsignal'
|
|
417
|
+
else
|
|
418
|
+
normalized
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Convert to display name (always v- prefix)
|
|
423
|
+
# @param input [String] Shortcut, key, or display name
|
|
424
|
+
# @return [String] Display name with v- prefix
|
|
425
|
+
def to_display(input)
|
|
426
|
+
expand(input)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Validate brand exists
|
|
430
|
+
# @param brand [String] Brand to validate
|
|
431
|
+
# @raise [BrandNotFoundError] if brand invalid
|
|
432
|
+
# @return [String] Config key if valid
|
|
433
|
+
def validate(brand)
|
|
434
|
+
key = to_config_key(brand)
|
|
435
|
+
brand_path = Config.brand_path(key)
|
|
436
|
+
|
|
437
|
+
unless Dir.exist?(brand_path)
|
|
438
|
+
available = Config.available_brands_display
|
|
439
|
+
raise BrandNotFoundError.new(brand, available)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
key
|
|
443
|
+
rescue StandardError => e
|
|
444
|
+
raise BrandNotFoundError, e.message
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**Migration Plan:**
|
|
454
|
+
```ruby
|
|
455
|
+
# PHASE 1: Create BrandResolver with tests
|
|
456
|
+
# PHASE 2: Update Config class to use BrandResolver
|
|
457
|
+
# PHASE 3: Update ProjectResolver to use BrandResolver
|
|
458
|
+
# PHASE 4: Update CLI (bin/dam) to use BrandResolver
|
|
459
|
+
# PHASE 5: Update remaining callers
|
|
460
|
+
# PHASE 6: Remove old methods from Config (mark deprecated first)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Testing Strategy:**
|
|
464
|
+
```ruby
|
|
465
|
+
RSpec.describe Appydave::Tools::Dam::BrandResolver do
|
|
466
|
+
include_context 'with vat filesystem and brands', brands: %w[appydave voz]
|
|
467
|
+
|
|
468
|
+
describe '.expand' do
|
|
469
|
+
it 'expands shortcut to display name' do
|
|
470
|
+
expect(described_class.expand('appydave')).to eq('v-appydave')
|
|
471
|
+
expect(described_class.expand('ad')).to eq('v-appydave')
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it 'leaves display names unchanged' do
|
|
475
|
+
expect(described_class.expand('v-appydave')).to eq('v-appydave')
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
describe '.normalize' do
|
|
480
|
+
it 'strips v- prefix' do
|
|
481
|
+
expect(described_class.normalize('v-appydave')).to eq('appydave')
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
it 'leaves normalized names unchanged' do
|
|
485
|
+
expect(described_class.normalize('appydave')).to eq('appydave')
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
describe '.to_config_key' do
|
|
490
|
+
it 'converts shortcuts to config keys' do
|
|
491
|
+
expect(described_class.to_config_key('ad')).to eq('appydave')
|
|
492
|
+
expect(described_class.to_config_key('joy')).to eq('beauty-and-joy')
|
|
493
|
+
expect(described_class.to_config_key('ss')).to eq('supportsignal')
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it 'handles display names' do
|
|
497
|
+
expect(described_class.to_config_key('v-appydave')).to eq('appydave')
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
describe '.validate' do
|
|
502
|
+
it 'validates existing brand' do
|
|
503
|
+
expect { described_class.validate('appydave') }.not_to raise_error
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it 'raises for invalid brand' do
|
|
507
|
+
expect { described_class.validate('invalid') }.to raise_error(BrandNotFoundError)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Success Criteria:**
|
|
514
|
+
- [ ] All brand transformation logic centralized
|
|
515
|
+
- [ ] Clear API with documented responsibilities
|
|
516
|
+
- [ ] All callers updated (10+ files)
|
|
517
|
+
- [ ] All tests pass
|
|
518
|
+
- [ ] No regression in brand resolution
|
|
519
|
+
|
|
520
|
+
**Rollback Plan:**
|
|
521
|
+
If issues arise, keep old methods and mark BrandResolver as experimental. Gradually migrate one file at a time.
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## Phase 3: Test Improvements (Priority 3)
|
|
526
|
+
|
|
527
|
+
### Task 3.1: Refactor ProjectListing Specs
|
|
528
|
+
**Effort:** 4 hours | **Priority:** Medium | **Risk:** Low
|
|
529
|
+
|
|
530
|
+
**Goal:** Standardize test patterns using shared filesystem contexts.
|
|
531
|
+
|
|
532
|
+
**Steps:**
|
|
533
|
+
1. Analyze `spec/appydave/tools/dam/project_listing_spec.rb`
|
|
534
|
+
2. Identify all Config mocks (20 lines)
|
|
535
|
+
3. Replace with `include_context 'with vat filesystem and brands'`
|
|
536
|
+
4. Update test expectations
|
|
537
|
+
5. Verify all tests pass
|
|
538
|
+
6. Document pattern in spec/support/README.md
|
|
539
|
+
|
|
540
|
+
**Before:**
|
|
541
|
+
```ruby
|
|
542
|
+
# Heavy mocking
|
|
543
|
+
allow(Appydave::Tools::Dam::Config).to receive_messages(projects_root: temp_root)
|
|
544
|
+
allow(Appydave::Tools::Dam::Config).to receive(:brand_path).with('appydave').and_return(brand1_path)
|
|
545
|
+
# ... 18 more allow() statements
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**After:**
|
|
549
|
+
```ruby
|
|
550
|
+
include_context 'with vat filesystem and brands', brands: %w[appydave voz]
|
|
551
|
+
|
|
552
|
+
before do
|
|
553
|
+
# Create real test projects
|
|
554
|
+
FileUtils.mkdir_p(File.join(appydave_path, 'b65-test'))
|
|
555
|
+
FileUtils.mkdir_p(File.join(voz_path, 'boy-baker'))
|
|
556
|
+
end
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Success Criteria:**
|
|
560
|
+
- [ ] Mock count reduced from 20 to <5
|
|
561
|
+
- [ ] All tests pass
|
|
562
|
+
- [ ] Test setup clearer and more maintainable
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Phase 4: Documentation & Cleanup (Priority 4)
|
|
567
|
+
|
|
568
|
+
### Task 4.1: Document Configuration Loading
|
|
569
|
+
**Effort:** 1 hour | **Priority:** Low | **Risk:** None
|
|
570
|
+
|
|
571
|
+
**Goal:** Add inline documentation explaining Config.configure memoization.
|
|
572
|
+
|
|
573
|
+
**Steps:**
|
|
574
|
+
1. Add module-level comment to `lib/appydave/tools/configuration/config.rb`
|
|
575
|
+
2. Document `configure` method behavior
|
|
576
|
+
3. Add examples to CLAUDE.md
|
|
577
|
+
|
|
578
|
+
**Implementation:**
|
|
579
|
+
```ruby
|
|
580
|
+
# lib/appydave/tools/configuration/config.rb
|
|
581
|
+
module Appydave
|
|
582
|
+
module Tools
|
|
583
|
+
module Configuration
|
|
584
|
+
# Central configuration management for appydave-tools
|
|
585
|
+
#
|
|
586
|
+
# Thread-safe singleton pattern with memoization.
|
|
587
|
+
# Calling `Config.configure` multiple times is safe and idempotent.
|
|
588
|
+
#
|
|
589
|
+
# @example Basic usage
|
|
590
|
+
# Config.configure # Load config (idempotent)
|
|
591
|
+
# Config.settings.video_projects_root
|
|
592
|
+
# Config.brands.get_brand('appydave')
|
|
593
|
+
#
|
|
594
|
+
# @example DAM module usage
|
|
595
|
+
# # Config.configure called once at module load
|
|
596
|
+
# # All subsequent calls are no-ops (memoized)
|
|
597
|
+
class Config
|
|
598
|
+
class << self
|
|
599
|
+
# Load configuration from JSON files
|
|
600
|
+
# Safe to call multiple times (idempotent/memoized)
|
|
601
|
+
# @return [void]
|
|
602
|
+
def configure
|
|
603
|
+
# Implementation uses @@configured flag for memoization
|
|
604
|
+
# ...
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Implementation Timeline
|
|
616
|
+
|
|
617
|
+
### Week 1 (Days 1-2): Quick Wins
|
|
618
|
+
- **Day 1 Morning:** Task 1.1 - Extract FileUtils
|
|
619
|
+
- **Day 1 Afternoon:** Task 1.2 - Define exception hierarchy
|
|
620
|
+
- **Day 2:** Review and test Phase 1 changes
|
|
621
|
+
|
|
622
|
+
### Week 2 (Days 3-5): Architectural Improvements
|
|
623
|
+
- **Day 3-4:** Task 2.1 - Extract GitHelper (test thoroughly)
|
|
624
|
+
- **Day 5:** Task 2.2 - Start BrandResolver
|
|
625
|
+
|
|
626
|
+
### Week 3 (Days 6-8): BrandResolver & Tests
|
|
627
|
+
- **Day 6-7:** Task 2.2 - Complete BrandResolver migration
|
|
628
|
+
- **Day 8:** Task 3.1 - Refactor specs
|
|
629
|
+
|
|
630
|
+
### Week 4 (Day 9): Documentation
|
|
631
|
+
- **Day 9:** Task 4.1 - Documentation and final review
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Risk Management
|
|
636
|
+
|
|
637
|
+
### High-Risk Tasks
|
|
638
|
+
1. **BrandResolver refactor** - Touches many files, complex logic
|
|
639
|
+
- Mitigation: Implement gradually, one file at a time
|
|
640
|
+
- Keep old methods during transition
|
|
641
|
+
- Extensive testing at each step
|
|
642
|
+
|
|
643
|
+
2. **GitHelper extraction** - Changes behavior of 3 classes
|
|
644
|
+
- Mitigation: Write tests first
|
|
645
|
+
- Test each class individually after migration
|
|
646
|
+
- Run full test suite after each change
|
|
647
|
+
|
|
648
|
+
### Medium-Risk Tasks
|
|
649
|
+
1. **Test refactoring** - Could break existing tests
|
|
650
|
+
- Mitigation: One test file at a time
|
|
651
|
+
- Keep git commits small for easy rollback
|
|
652
|
+
|
|
653
|
+
### Low-Risk Tasks
|
|
654
|
+
1. **FileUtils extraction** - Pure utility code
|
|
655
|
+
2. **Exception hierarchy** - Additive changes
|
|
656
|
+
3. **Documentation** - No code changes
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Success Metrics
|
|
661
|
+
|
|
662
|
+
### Code Quality Metrics (Before → After)
|
|
663
|
+
- **Duplicated lines:** 150 → <30 (80% reduction)
|
|
664
|
+
- **Mock density in tests:** 8% → <3%
|
|
665
|
+
- **Files with brand logic:** 5 → 1 (BrandResolver)
|
|
666
|
+
- **Test coverage:** Maintain >90%
|
|
667
|
+
|
|
668
|
+
### Behavioral Metrics
|
|
669
|
+
- [ ] All existing tests pass
|
|
670
|
+
- [ ] No regression in CLI behavior
|
|
671
|
+
- [ ] No breaking changes to public API
|
|
672
|
+
- [ ] Gem version: Minor bump (backward compatible)
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## Commit Strategy
|
|
677
|
+
|
|
678
|
+
Use semantic commit messages for automated versioning:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Phase 1
|
|
682
|
+
kfeat "extract FileUtils module for directory size calculations"
|
|
683
|
+
kfeat "add DAM exception hierarchy for consistent error handling"
|
|
684
|
+
|
|
685
|
+
# Phase 2
|
|
686
|
+
kfeat "extract GitHelper module to eliminate 90 lines of duplication"
|
|
687
|
+
kfeat "create BrandResolver to centralize brand transformation logic"
|
|
688
|
+
|
|
689
|
+
# Phase 3
|
|
690
|
+
kfix "refactor project_listing_spec to use shared filesystem contexts"
|
|
691
|
+
|
|
692
|
+
# Phase 4
|
|
693
|
+
kdocs "add configuration loading documentation and memoization notes"
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## Getting Help
|
|
699
|
+
|
|
700
|
+
If you encounter issues:
|
|
701
|
+
1. **Check report:** [report-2025-01-21.md](./report-2025-01-21.md)
|
|
702
|
+
2. **Run tests:** `bundle exec rspec`
|
|
703
|
+
3. **Verify changes:** `git diff`
|
|
704
|
+
4. **Rollback:** `git reset --hard HEAD`
|
|
705
|
+
|
|
706
|
+
**Need clarification?** Ask before implementing high-risk changes.
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
**Plan Created:** 2025-01-21
|
|
711
|
+
**Estimated Total Effort:** 18-24 hours across 2 weeks
|
|
712
|
+
**Risk Level:** Medium (manageable with phased approach)
|