appydave-tools 0.76.7 → 0.76.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51ac1542f215f90d518a24c96b5185cf954b9db2beab4bbb93c8d43f3b106fb7
4
- data.tar.gz: 11acb6d8817125846945e4f7ba3bfb77b305155093f507f346a21477c4102cdb
3
+ metadata.gz: 837cfa631ce3f72655208add542964a605efa385aea01abfe49c55fb9110cdbb
4
+ data.tar.gz: 4adfd72dad7dc7a41f909857a21616d3385b259ee11969d94641c75731713924
5
5
  SHA512:
6
- metadata.gz: 02a5b3623de62bbb09d343101c1edc48b711fc8682d9396689043afcdd134276c887de57d802e8a717db9adaeb34b48eb14dd6a199e085d64430a0525300cf1e
7
- data.tar.gz: ce60ac62a7753a206768335c7cc51c8d4765223efd9dd34d4a490fc250f8af63aca11fd532f9918f15ac075be8e7698bd7613704040a16f66777a0455ca54c25
6
+ metadata.gz: 2748e05e28b1c0cd4a7e6ec99f13a513c68ec49115d6b90bd0090d25467958eb0dd759cbf6fb8b94f210fc1835ee5d2c77cac79e3d7043cac2127513d982d89f
7
+ data.tar.gz: cf039ebec627c4899f5abc6c309829894c4dbca2ec71daf6ceb1143f61b72a46edb480223c9a8ab6acd5892ef343980f5395d6f6363276352056f111c3753026
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [0.76.7](https://github.com/appydave/appydave-tools/compare/v0.76.6...v0.76.7) (2026-03-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * extract S3ArgParser module from VatCLI; add spec ([9bbd709](https://github.com/appydave/appydave-tools/commit/9bbd7099c26b774b68103f4d37d0a25f22f4abc2))
7
+
1
8
  ## [0.76.6](https://github.com/appydave/appydave-tools/compare/v0.76.5...v0.76.6) (2026-03-19)
2
9
 
3
10
 
@@ -45,7 +45,7 @@ kfeat "add feature description" # Minor version bump
45
45
  kfix "fix bug description" # Patch version bump
46
46
  ```
47
47
 
48
- **Baseline (2026-03-19):** 831 examples, 0 failures, ~85.92% line coverage
48
+ **Baseline (2026-03-19):** 847 examples, 0 failures, ~86.21% line coverage
49
49
 
50
50
  ---
51
51
 
@@ -221,9 +221,9 @@ Appydave::Tools::Configuration::Config.configure
221
221
 
222
222
  ## Quality Gates
223
223
 
224
- - **Tests:** `bundle exec rspec` — 831 examples, 0 failures (do not ship if any fail)
224
+ - **Tests:** `bundle exec rspec` — 847 examples, 0 failures (do not ship if any fail)
225
225
  - **Lint:** `bundle exec rubocop --format clang` — 0 offenses (CI will reject)
226
- - **Coverage:** ≥ 85.92% line coverage
226
+ - **Coverage:** ≥ 86.21% line coverage
227
227
  - **frozen_string_literal:** Required on every new `.rb` file
228
228
  - **Commit format:** `kfeat`/`kfix` only — triggers semantic versioning + CI wait
229
229
 
@@ -248,6 +248,12 @@ Appydave::Tools::Configuration::Config.configure
248
248
  - **Jump Commands layer is undertested:** `Commands::Remove`, `Commands::Add`, `Commands::Update` have zero dedicated specs. Auto-regenerate CLI spec does not substitute for command-layer unit tests verifying `--force` guards, error codes, and suggestion logic (see B018).
249
249
  - **Jump report commands** got `--limit` and `--skip-unassigned` flags after initial implementation. Jump tool scope grows incrementally.
250
250
 
251
+ ### From extract-vat-cli (2026-03-19)
252
+
253
+ - **Do NOT carry over rubocop-disable comments when extracting methods.** Run rubocop on the new file first — methods that needed disables in a 1,600-line God class often don't exceed thresholds in a properly-scoped library class. Carrying them over causes CI to flag `Lint/RedundantCopDisableDirective` and requires a second fix commit.
254
+ - **grep for callers before writing the plan.** `format_bytes` had 4 callers in bin/dam, not 3 as counted from memory. The orphaned-projects loop was missed.
255
+ - **`valid_brand?` needs `Config.brands` mock, not just `SettingsConfig` mock.** The shared `'with vat filesystem and brands'` context only mocks `SettingsConfig#video_projects_root`. Any method calling `Config.brands` directly needs an explicit brands config mock.
256
+
251
257
  ### From Three-Lens Audit (2026-03-19)
252
258
 
253
259
  - **`file_collector.rb` has two landmines before FR-2:** `puts @working_directory` at line 15 pollutes stdout; `FileUtils.cd` without `ensure` leaves process in wrong directory on exception. Fix both before adding any code to this class.
@@ -1,7 +1,7 @@
1
1
  # Project Backlog — AppyDave Tools
2
2
 
3
- **Last updated**: 2026-03-19 (micro-cleanup campaign complete)
4
- **Total**: 33 | Pending: 7 | In Progress: 1 | Done: 25 | Deferred: 0 | Rejected: 0
3
+ **Last updated**: 2026-03-19 (extract-vat-cli campaign complete)
4
+ **Total**: 37 | Pending: 11 | Done: 26 | Deferred: 0 | Rejected: 0
5
5
 
6
6
  ---
7
7
 
@@ -14,8 +14,12 @@
14
14
  - [ ] B008 — Performance: cache git/S3 status with 5-min TTL | Priority: low
15
15
  - [ ] B009 — UX: progress indicators for dam operations > 5s | Priority: low
16
16
  - [ ] B010 — UX: auto-adjust dam table column widths to terminal width | Priority: low
17
- - [~] B011 — Arch: extract VatCLI business logic from bin/dam (1,600-line God class) | Campaign: extract-vat-cli
17
+ - [x] B011 — Arch: extract VatCLI business logic from bin/dam (1,600-line God class) | Completed: extract-vat-cli (2026-03-19)
18
18
  - [ ] B020 — Arch: split S3Operations (1,030 lines, mixed I/O + logic) | Priority: low
19
+ - [ ] B034 — Fix: replace exit 1 with typed exceptions in S3ScanCommand + S3ArgParser | Priority: high (blocks B007 + test coverage)
20
+ - [ ] B035 — Fix: remove ENV['BRAND_PATH'] side effect from S3ArgParser | Priority: high (blocks B007 parallelism)
21
+ - [ ] B036 — Tests: improve S3ScanCommand spec from D to B (depends on B034) | Priority: medium
22
+ - [ ] B037 — Tests: LocalSyncStatus :partial case, local_file_count assertion, Zone.Identifier exclusion | Priority: medium
19
23
 
20
24
  ---
21
25
 
@@ -5,13 +5,13 @@
5
5
  **Target**: All 4 complete; 831+ examples passing; rubocop 0 offenses; no regressions
6
6
 
7
7
  ## Summary
8
- - Total: 4 | Complete: 0 | In Progress: 0 | Pending: 4 | Failed: 0
8
+ - Total: 4 | Complete: 4 | In Progress: 0 | Pending: 0 | Failed: 0
9
9
 
10
10
  ## Pending
11
11
  - [x] extract-format-bytes — Replaced 4 callers (plan said 3; orphaned-projects loop in display_s3_scan_table was a 4th). format_bytes deleted. rubocop 0 offenses. Commit: 3cd362f.
12
12
  - [x] extract-local-sync-status — LocalSyncStatus module created, 7 specs added (838 total), both methods gone from VatCLI. Side-fix: restored youtube_automation_config require incorrectly removed in prior commit. v0.76.5.
13
13
  - [x] extract-s3-scan-command — S3ScanCommand created, 2 smoke tests added (840 total), 3 methods gone from VatCLI. Note: rubocop-disable directives became redundant once methods left God class — needed 2nd kfix to remove them. v0.76.6.
14
- - [~] extract-s3-arg-parser — Extract `parse_s3_args` + `valid_brand?` + `parse_share_args` + `show_share_usage_and_exit` + `parse_discover_args` new `S3ArgParser` class; add spec
14
+ - [x] extract-s3-arg-parser — S3ArgParser module created, 7 specs added (847 total), 5 methods + 8 callers updated, all gone from VatCLI. Note: valid_brand? needed Config.brands mock (shared context only mocks SettingsConfig). v0.76.7.
15
15
 
16
16
  ## In Progress
17
17
 
@@ -0,0 +1,107 @@
1
+ # Assessment: extract-vat-cli
2
+
3
+ **Campaign**: extract-vat-cli
4
+ **Date**: 2026-03-19 → 2026-03-19
5
+ **Results**: 4/4 complete, 0 failed
6
+
7
+ ---
8
+
9
+ ## Results Summary
10
+
11
+ | Work Unit | Outcome | Version | Notes |
12
+ |---|---|---|---|
13
+ | extract-format-bytes | ✅ Complete | v0.76.4→ | 4 callers replaced (plan said 3; orphaned-projects loop was 4th) |
14
+ | extract-local-sync-status | ✅ Complete | v0.76.5 | Side-fix: restored youtube_automation_config require removed in prior commit |
15
+ | extract-s3-scan-command | ✅ Complete | v0.76.6 | Needed 2nd kfix: inherited rubocop-disable directives became redundant |
16
+ | extract-s3-arg-parser | ✅ Complete | v0.76.7 | valid_brand? needed BrandsConfig mock; parse_share fully tested |
17
+
18
+ **Suite:** 831 → 847 examples (+16), 0 failures, 86.21% coverage. rubocop 0 offenses. CI green on all 4 commits.
19
+
20
+ ---
21
+
22
+ ## What Worked Well
23
+
24
+ 1. **Sequential wave strategy was correct.** All 4 work units touched bin/dam — wave size = 1 was the only safe option. No conflicts, no merge issues, zero failed agents.
25
+
26
+ 2. **AGENTS.md quality was sufficient.** All agents executed cleanly without confusion about which methods to move or which callers to update. The detailed WU-specific instructions in the campaign AGENTS.md were the right level of specificity.
27
+
28
+ 3. **format_bytes extraction was genuinely trivial.** FileHelper.format_size already existed and was identical. WU1 was pure deletion + caller update — no logic risk.
29
+
30
+ 4. **LocalSyncStatus came out clean.** The module_function pattern worked correctly, the shared filesystem context covered most branches, and the side-fix of youtube_automation_config was a net win.
31
+
32
+ 5. **bin/dam went from 1,600 → 1,223 lines (−23%) and 20+ → 5 rubocop-disables.** The remaining 5 are structural (help text dispatch, not logic). Significant improvement.
33
+
34
+ ---
35
+
36
+ ## What Didn't Work
37
+
38
+ 1. **Inherited rubocop-disable directives caused 2nd kfix commits** (WU3, WU4). Methods that needed `Metrics/MethodLength` disable in the God class context don't exceed thresholds in properly-scoped library classes. CI rubocop 1.85.1 flags them as redundant. Rule: don't carry over rubocop-disable comments when extracting — run rubocop fresh on the new file.
39
+
40
+ 2. **Caller count was wrong in the plan.** Plan said 3 callers for format_bytes; there were 4 (orphaned-projects loop in display_s3_scan_table). This was a minor issue (caught and fixed by WU1 agent) but points to a planning discipline gap: grep for callers, don't count from memory.
41
+
42
+ 3. **exit 1 calls were carried into library code.** `S3ScanCommand` and `S3ArgParser` both call `exit 1` in response to invalid inputs. This was the pre-existing VatCLI pattern — the extraction was faithful — but it now lives in library classes where it's a boundary violation. Makes testing error paths impossible and blocks safe parallelism (B007).
43
+
44
+ 4. **S3ScanCommand spec is F-grade (confirmed by independent test audit).** Two `respond_to` tests confirm the class loads; nothing more. 90 lines of orchestration logic — manifest merging, orphan detection, LocalSyncStatus wiring — are completely unprotected. The `exit 1` call at line 55 makes the "no manifest" path untestable without production code changes first.
45
+
46
+ 5. **`Zone.Identifier` exclusion in LocalSyncStatus has no test.** The filter for Windows metadata files (line 26) is untested — removal or inversion would pass all current specs silently.
47
+
48
+ 6. **`ENV['BRAND_PATH']` side-effect never asserted in S3ArgParser spec.** All three parse methods set it; none of the specs check it. A key-name change would silently break ConfigLoader downstream.
49
+
50
+ ---
51
+
52
+ ## Key Learnings — Application
53
+
54
+ 1. **Don't carry over rubocop-disable comments when extracting.** Run rubocop fresh on the new file; only add disables if actually triggered.
55
+ 2. **grep for callers before writing the plan.** Don't count from memory or code review alone.
56
+ 3. **Library classes must not call `exit`.** Raise typed exceptions (`ConfigurationError`, `UsageError`) — let the CLI layer print and exit. Required before B007.
57
+ 4. **`valid_brand?` and anything calling `Config.brands` needs BrandsConfig mock** — the shared context only mocks `SettingsConfig#video_projects_root`.
58
+ 5. **ENV['BRAND_PATH'] side effect now in library code.** S3ArgParser sets a process-wide env var as a side effect of argument parsing. Must be resolved before parallelism (B007).
59
+
60
+ ---
61
+
62
+ ## Key Learnings — Ralph Loop
63
+
64
+ 1. **Wave size = 1 was correct for God-class extraction.** When all work units share a single target file, parallel waves cause conflicts. Plan for sequential from the start.
65
+ 2. **Detailed WU-specific AGENTS.md sections paid off.** Specifying exact line numbers, method names, and new class signatures eliminated agent confusion. Worth the upfront effort.
66
+ 3. **Side-fixes happen.** WU2 found and fixed a pre-existing require omission. Good — agents should fix what they find. Update the plan notes so the next session knows.
67
+ 4. **2nd kfix commits were needed on 2/4 work units** — predictable pattern for extractions. Build in a "check for redundant rubocop directives" step in future extraction AGENTS.md.
68
+
69
+ ---
70
+
71
+ ## Promote to Main KDD?
72
+
73
+ - "Don't carry over rubocop-disable when extracting" → yes, promote
74
+ - "grep for callers before planning" → yes, promote
75
+ - "Library classes must not call exit" → yes, promote (already in project AGENTS.md, promote to KDD)
76
+
77
+ ---
78
+
79
+ ## Suggestions for Next Campaign
80
+
81
+ ### Debt introduced by this campaign (should address before B007)
82
+
83
+ **B034 — Fix: replace `exit 1` with typed exceptions in S3ScanCommand + S3ArgParser**
84
+ - `S3ScanCommand#scan_single` calls `exit 1` at line 55 (no manifest path)
85
+ - `S3ArgParser` calls `exit 1` at 4 locations (invalid brand, PWD auto-detect fail, discover missing args, share missing args)
86
+ - Fix: raise `Appydave::Tools::Dam::ConfigurationError` / new `UsageError` subclass
87
+ - VatCLI `rescue StandardError` already handles these at command level
88
+ - Unblocks proper testing of error paths AND safe parallelism
89
+
90
+ **B035 — Fix: remove ENV['BRAND_PATH'] side effect from S3ArgParser**
91
+ - `parse_s3`, `parse_share`, `parse_discover` all set `ENV['BRAND_PATH']` as a side effect
92
+ - Acceptable in a single-process CLI; unsafe in parallel execution
93
+ - Fix: return the brand_path in the result hash and let VatCLI set it, or extract to explicit `S3ArgParser.configure_env!(brand)`
94
+
95
+ **B036 — Tests: improve S3ScanCommand spec from D to B**
96
+ - Depends on B034 (exit → exception) — can't test "no manifest" path until that's fixed
97
+ - Add: mocked S3Scanner + filesystem fixture, assert manifest merge logic, assert LocalSyncStatus wiring
98
+
99
+ **B037 — Tests: add LocalSyncStatus :partial case + local_file_count assertion**
100
+ - Add `:partial` context (1 file in staging, s3 has 3) — assert `:partial` status and `local_file_count: 1`
101
+ - Add assertion that `:synced` case sets `local_file_count` correctly
102
+
103
+ ### Mode recommendation for next session
104
+
105
+ **4. Extend** — these are small debt items, same stack, inherit this AGENTS.md.
106
+
107
+ Or, if you want to move forward to B007 (parallelism) instead: address B034 + B035 first (1-wave campaign), then B007 becomes buildable.
@@ -0,0 +1,36 @@
1
+ # Wave Learnings — extract-vat-cli
2
+
3
+ **Campaign:** extract-vat-cli
4
+ **Date:** 2026-03-19
5
+ **Result:** 4/4 complete. 831 → 847 examples (+16). v0.76.3 → v0.76.7.
6
+
7
+ ---
8
+
9
+ ## Application Learnings
10
+
11
+ ### 1. format_bytes had 4 callers, not 3
12
+ The orphaned-projects loop in `display_s3_scan_table` had its own `format_bytes` call. Plan said 3 callers; there were 4. Always grep for callers before writing the plan rather than counting from memory.
13
+
14
+ ### 2. rubocop-disable directives become redundant when methods leave God class
15
+ WU3 and WU4 both needed a second `kfix` to remove `rubocop:disable Metrics/MethodLength` and `Metrics/CyclomaticComplexity` directives that were valid in the 1,600-line VatCLI context but flagged as redundant by CI rubocop 1.85.1 once the methods were in properly-scoped library classes.
16
+
17
+ **Rule for future extractions:** Do NOT carry over rubocop-disable comments. Run rubocop locally on the new file first — only add disables if rubocop actually flags an offense.
18
+
19
+ ### 3. valid_brand? needed Config.brands mock, not just SettingsConfig mock
20
+ The shared context `'with vat filesystem and brands'` only mocks `SettingsConfig#video_projects_root`. Testing `valid_brand?` (which calls `Config.brands`) required an explicit `Config.configure` + `Config.brands` mock with a test `BrandsConfig` instance. Document this in AGENTS.md for future S3ArgParser specs.
21
+
22
+ ### 4. youtube_automation_config require was missing (pre-existing)
23
+ WU2 agent discovered that a prior `chore(cleanup)` commit had incorrectly removed `require 'appydave/tools/youtube_automation_config'` from `lib/appydave/tools.rb`. The CI was failing on a `NameError` for `YoutubeAutomationConfig` unrelated to our work. Agent restored it as a side-fix. This was confirmed pre-existing in WU1's CI output.
24
+
25
+ ---
26
+
27
+ ## Loop Meta-Learnings
28
+
29
+ ### Wave size = 1 was correct
30
+ All 4 work units touched `bin/dam`. Sequential was the only safe option. No conflicts, no merge issues.
31
+
32
+ ### Agents produced clean rubocop output when not inheriting disables
33
+ WU1 (no rubocop disables to inherit) was cleanest. WU2 produced clean output first-pass. WU3 and WU4 needed a second pass due to inherited directives — preventable with the "don't carry over disables" rule above.
34
+
35
+ ### 4 agents, all successful, no failures
36
+ Every work unit completed in a single run (plus occasional second kfix for rubocop cleanup). AGENTS.md quality was sufficient — agents didn't get confused about which methods to move or which callers to update.
@@ -0,0 +1,653 @@
1
+ # AGENTS.md — AppyDave Tools / library-boundary-cleanup campaign
2
+
3
+ > Operational knowledge for every background agent. Self-contained — you receive only this file + your work unit prompt.
4
+ > Inherited from: extract-vat-cli campaign (2026-03-19)
5
+ > Updated for: library-boundary-cleanup campaign (2026-03-19)
6
+
7
+ ---
8
+
9
+ ## Project Overview
10
+
11
+ **What:** Ruby gem providing CLI productivity tools for AppyDave's YouTube content creation workflow.
12
+ **Stack:** Ruby 3.4.2, Bundler 2.6.2, RSpec, RuboCop, semantic-release CI/CD.
13
+ **Active campaign:** library-boundary-cleanup — fixing two boundary violations left by extract-vat-cli:
14
+ 1. `exit 1` calls inside library code (S3ScanCommand + S3ArgParser)
15
+ 2. `ENV['BRAND_PATH']` side-effect inside library argument parser (S3ArgParser)
16
+ **Commits:** Trigger automated semantic versioning via GitHub Actions. Always use `kfeat`/`kfix` — never `git commit`.
17
+
18
+ ---
19
+
20
+ ## ⚠️ Pre-Commit Check (Mandatory Every Commit)
21
+
22
+ Before running `kfix`, always run:
23
+ ```bash
24
+ git status
25
+ ```
26
+ Confirm ONLY the files you intentionally changed are staged. If unexpected files appear, run `git diff` to investigate before proceeding. Never commit files you didn't intentionally change.
27
+
28
+ **Why:** Prior campaign accidentally staged a pre-existing uncommitted change when running `kfix`. Required a follow-up fix commit.
29
+
30
+ ---
31
+
32
+ ## Build & Run Commands
33
+
34
+ ```bash
35
+ # Initialize rbenv (required if rbenv not in PATH)
36
+ eval "$(rbenv init -)"
37
+
38
+ # Run tests
39
+ bundle exec rspec # All tests
40
+ bundle exec rspec spec/path/to/file_spec.rb # Single file
41
+ RUBYOPT="-W0" bundle exec rspec # Suppress Ruby 3.4 platform warnings
42
+
43
+ # Lint
44
+ bundle exec rubocop --format clang # Standard lint check (matches CI)
45
+
46
+ # Commit (never use git commit directly)
47
+ kfeat "add feature description" # Minor version bump
48
+ kfix "fix bug description" # Patch version bump
49
+ ```
50
+
51
+ **Baseline (start of library-boundary-cleanup):** 847 examples, 0 failures, ~85.92% line coverage
52
+
53
+ ---
54
+
55
+ ## Directory Structure
56
+
57
+ ```
58
+ bin/
59
+ dam Main DAM CLI entry point (VatCLI class — 1,600 lines, being reduced)
60
+ exe/ Thin wrappers for gem installation (no .rb extension)
61
+ lib/appydave/tools/
62
+ dam/ Digital Asset Management — main active area
63
+ brand_resolver.rb Centralizes ALL brand name transformations (appydave ↔ v-appydave)
64
+ errors.rb Custom exception hierarchy — ADD UsageError here in B034
65
+ fuzzy_matcher.rb Levenshtein distance for "did you mean?" suggestions
66
+ git_helper.rb Extracted git command wrappers
67
+ file_helper.rb File utility methods (format_size, calculate_directory_size, format_age)
68
+ config.rb Delegates brand resolution to BrandResolver; memoized Config loading
69
+ project_resolver.rb Project name resolution with regex pattern matching
70
+ project_listing.rb Table display for `dam list` command
71
+ s3_operations.rb S3 upload/download/status with MD5 comparison
72
+ s3_scanner.rb S3 bucket scanner for s3-scan command
73
+ status.rb Project git/S3 status display
74
+ manifest_generator.rb Video project manifest
75
+ sync_from_ssd.rb SSD sync operations
76
+ ssd_status.rb SSD backup status
77
+ share_operations.rb Pre-signed URL generation
78
+ config_loader.rb Loads .video-tools.env per brand
79
+ local_sync_status.rb Enriches project data with local s3-staging sync status
80
+ s3_scan_command.rb S3 scan orchestration + display — HAS exit 1 AT LINE 55 (B034 target)
81
+ s3_arg_parser.rb CLI argument parsing for S3 commands — HAS exit 1 + ENV side-effects (B034/B035 targets)
82
+ lib/appydave/tools.rb Require file — already includes s3_scan_command and s3_arg_parser
83
+ spec/
84
+ appydave/tools/dam/ One spec file per dam/ class
85
+ support/
86
+ dam_filesystem_helpers.rb Shared contexts for DAM filesystem testing
87
+ ```
88
+
89
+ ---
90
+
91
+ ## This Campaign: What We're Fixing
92
+
93
+ ### The Two Problems
94
+
95
+ **Problem 1: `exit 1` in library code** — Library classes must not terminate the process. VatCLI's `rescue StandardError` already catches and prints errors. Fix: replace `exit 1` with typed exceptions.
96
+
97
+ **Problem 2: `ENV['BRAND_PATH']` side-effect in library** — `S3ArgParser` sets a process-wide env var during argument parsing. Unsafe for parallelism. Fix: return `brand_path:` in result hash; move the `ENV[]=` call to VatCLI.
98
+
99
+ ### Work Unit Dependencies
100
+
101
+ ```
102
+ B034: extract-exit-calls (no deps — do first)
103
+ B035: extract-env-side-effect (DEPENDS ON B034 — both touch s3_arg_parser.rb)
104
+ B036: tests-s3-scan-command (DEPENDS ON B034 — can't test exceptions until they exist)
105
+ B037: tests-local-sync-status (no deps — but run after B035 for clean baseline)
106
+
107
+ Wave 1: B034
108
+ Wave 2: B035
109
+ Wave 3: B036 + B037 (parallel — different spec files)
110
+ ```
111
+
112
+ ---
113
+
114
+ ## B034: extract-exit-calls
115
+
116
+ **Files touched:** `lib/appydave/tools/dam/errors.rb`, `lib/appydave/tools/dam/s3_scan_command.rb`, `lib/appydave/tools/dam/s3_arg_parser.rb`
117
+
118
+ ### Step 1 — Add UsageError to errors.rb
119
+
120
+ After the existing `GitOperationError` line, add:
121
+
122
+ ```ruby
123
+ # Raised when CLI arguments are invalid or missing
124
+ class UsageError < DamError; end
125
+ ```
126
+
127
+ ### Step 2 — Fix S3ScanCommand (line 55)
128
+
129
+ Replace:
130
+ ```ruby
131
+ unless File.exist?(manifest_path)
132
+ puts "❌ Manifest not found: #{manifest_path}"
133
+ puts " Run: dam manifest #{brand_key}"
134
+ puts " Then retry: dam s3-scan #{brand_key}"
135
+ exit 1
136
+ end
137
+ ```
138
+
139
+ With:
140
+ ```ruby
141
+ unless File.exist?(manifest_path)
142
+ raise Appydave::Tools::Dam::ConfigurationError,
143
+ "Manifest not found: #{manifest_path}. Run: dam manifest #{brand_key}"
144
+ end
145
+ ```
146
+
147
+ Remove the `puts` lines above — the exception message carries the information. VatCLI's rescue prints it.
148
+
149
+ ### Step 3 — Fix S3ArgParser (4 exit locations)
150
+
151
+ **parse_s3 — PWD auto-detect fail (around line 25):**
152
+
153
+ Replace:
154
+ ```ruby
155
+ if brand.nil? || project_id.nil?
156
+ puts '❌ Could not auto-detect brand/project from current directory'
157
+ puts "Usage: dam #{command} <brand> <project> [--dry-run]"
158
+ exit 1
159
+ end
160
+ ```
161
+
162
+ With:
163
+ ```ruby
164
+ raise Appydave::Tools::Dam::UsageError,
165
+ "Could not auto-detect brand/project from current directory. Usage: dam #{command} <brand> <project> [--dry-run]" if brand.nil? || project_id.nil?
166
+ ```
167
+
168
+ **parse_s3 — invalid brand (around line 43):**
169
+
170
+ Replace:
171
+ ```ruby
172
+ unless valid_brand?(brand_arg)
173
+ puts "❌ Invalid brand: '#{brand_arg}'"
174
+ puts ''
175
+ puts 'Valid brands:'
176
+ # ... puts lines ...
177
+ puts "Usage: dam #{command} <brand> <project> [--dry-run]"
178
+ exit 1
179
+ end
180
+ ```
181
+
182
+ With:
183
+ ```ruby
184
+ unless valid_brand?(brand_arg)
185
+ raise Appydave::Tools::Dam::UsageError,
186
+ "Invalid brand: '#{brand_arg}'. Valid brands: appydave, voz, aitldr, kiros, joy, ss. Usage: dam #{command} <brand> <project> [--dry-run]"
187
+ end
188
+ ```
189
+
190
+ **parse_discover — missing args (around line 100):**
191
+
192
+ Replace:
193
+ ```ruby
194
+ if brand_arg.nil? || project_arg.nil?
195
+ puts 'Usage: dam s3-discover <brand> <project> [--shareable]'
196
+ # ... puts lines ...
197
+ exit 1
198
+ end
199
+ ```
200
+
201
+ With:
202
+ ```ruby
203
+ raise Appydave::Tools::Dam::UsageError,
204
+ 'Usage: dam s3-discover <brand> <project> [--shareable]' if brand_arg.nil? || project_arg.nil?
205
+ ```
206
+
207
+ **show_share_usage_and_exit — rename and replace:**
208
+
209
+ Rename the method to `raise_share_usage_error`. Replace:
210
+ ```ruby
211
+ def show_share_usage_and_exit
212
+ puts 'Usage: dam s3-share ...'
213
+ # ... puts lines ...
214
+ exit 1
215
+ end
216
+ ```
217
+
218
+ With:
219
+ ```ruby
220
+ def raise_share_usage_error
221
+ raise Appydave::Tools::Dam::UsageError,
222
+ 'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
223
+ end
224
+ ```
225
+
226
+ Update the caller in `parse_share`:
227
+ ```ruby
228
+ # Before:
229
+ show_share_usage_and_exit if brand_arg.nil? || project_arg.nil? || file_arg.nil?
230
+ # After:
231
+ raise_share_usage_error if brand_arg.nil? || project_arg.nil? || file_arg.nil?
232
+ ```
233
+
234
+ ### Done when B034 is complete
235
+ - `errors.rb` contains `UsageError < DamError`
236
+ - 0 occurrences of `exit 1` in `s3_scan_command.rb` or `s3_arg_parser.rb`
237
+ - `show_share_usage_and_exit` renamed to `raise_share_usage_error`
238
+ - `bundle exec rubocop --format clang` → 0 offenses
239
+ - `RUBYOPT="-W0" bundle exec rspec` → 847 examples, 0 failures (no new specs in this WU)
240
+ - `git status` clean before `kfix`
241
+
242
+ ---
243
+
244
+ ## B035: extract-env-side-effect
245
+
246
+ **Files touched:** `lib/appydave/tools/dam/s3_arg_parser.rb`, `bin/dam`
247
+
248
+ **Prerequisite:** B034 complete.
249
+
250
+ ### Step 1 — Remove ENV side-effects from S3ArgParser
251
+
252
+ In `parse_s3` (around line 51): remove `ENV['BRAND_PATH'] = ...`. Add `brand_path:` to returned hash:
253
+
254
+ ```ruby
255
+ # Before:
256
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
257
+ { brand: brand_key, project: project_id, dry_run: dry_run, force: force }
258
+
259
+ # After (auto-detect path):
260
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
261
+ { brand: brand_key, project: project_id, dry_run: dry_run, force: force, brand_path: brand_path }
262
+ ```
263
+
264
+ Note: In the `brand_arg.nil?` branch, `brand` is set from `ProjectResolver.detect_from_pwd` — use `brand` not `brand_arg` for path resolution there.
265
+
266
+ In `parse_share` (around line 82): remove `ENV['BRAND_PATH'] = ...`. Add `brand_path:` to returned hash:
267
+
268
+ ```ruby
269
+ # Before:
270
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
271
+ { brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download }
272
+
273
+ # After:
274
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
275
+ { brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download, brand_path: brand_path }
276
+ ```
277
+
278
+ In `parse_discover` (around line 108): same pattern:
279
+
280
+ ```ruby
281
+ # Before:
282
+ ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
283
+ { brand_key: brand_key, project_id: project_id, shareable: shareable }
284
+
285
+ # After:
286
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
287
+ { brand_key: brand_key, project_id: project_id, shareable: shareable, brand_path: brand_path }
288
+ ```
289
+
290
+ ### Step 2 — Update VatCLI callers in bin/dam
291
+
292
+ Search for every call to `S3ArgParser.parse_s3`, `S3ArgParser.parse_share`, `S3ArgParser.parse_discover` in `bin/dam`. After each call, add:
293
+
294
+ ```ruby
295
+ options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-up')
296
+ ENV['BRAND_PATH'] = options[:brand_path]
297
+ ```
298
+
299
+ Same pattern for `parse_share` and `parse_discover` callers.
300
+
301
+ **Affected VatCLI methods** (grep `S3ArgParser` in bin/dam to confirm):
302
+ - `s3_up_command` → uses `parse_s3`
303
+ - `s3_down_command` → uses `parse_s3`
304
+ - `s3_status_command` → uses `parse_s3`
305
+ - `s3_cleanup_remote_command` → uses `parse_s3`
306
+ - `s3_cleanup_local_command` → uses `parse_s3`
307
+ - `archive_command` → uses `parse_s3`
308
+ - `s3_share_command` → uses `parse_share`
309
+ - `s3_discover_command` → uses `parse_discover`
310
+
311
+ ### Done when B035 is complete
312
+ - 0 occurrences of `ENV['BRAND_PATH'] =` in `s3_arg_parser.rb`
313
+ - All 3 parse methods return `brand_path:` in their result hash
314
+ - All VatCLI callers set `ENV['BRAND_PATH'] = options[:brand_path]` after parsing
315
+ - `bundle exec rubocop --format clang` → 0 offenses
316
+ - `RUBYOPT="-W0" bundle exec rspec` → 847 examples, 0 failures
317
+ - `git status` clean before `kfix`
318
+
319
+ ---
320
+
321
+ ## B036: tests-s3-scan-command
322
+
323
+ **File touched:** `spec/appydave/tools/dam/s3_scan_command_spec.rb`
324
+
325
+ **Prerequisite:** B034 complete (exceptions must exist before testing them).
326
+
327
+ Current state: 2 smoke tests (`respond_to` checks). Replace with real behaviour tests.
328
+
329
+ ### Target: 8–10 examples covering
330
+
331
+ ```ruby
332
+ RSpec.describe Appydave::Tools::Dam::S3ScanCommand do
333
+ include_context 'with vat filesystem and brands', brands: %w[appydave]
334
+
335
+ let(:scanner) { described_class.new }
336
+ let(:brand_key) { 'appydave' }
337
+
338
+ before do
339
+ allow(Appydave::Tools::Dam::S3Scanner).to receive(:new).and_return(mock_s3_scanner)
340
+ allow(Appydave::Tools::Configuration::Config).to receive(:configure)
341
+ allow(Appydave::Tools::Configuration::Config).to receive(:brands).and_return(mock_brands_config)
342
+ end
343
+
344
+ describe '#scan_single' do
345
+ context 'when manifest exists and S3 returns results' do
346
+ # Write a fixture projects.json to brand_path
347
+ # Mock scanner.scan_all_projects to return { 'b65-test' => { file_count: 3, total_bytes: 1000, last_modified: '...' } }
348
+ # Expect: manifest updated, updated_count == 1, no error
349
+ end
350
+
351
+ context 'when manifest not found' do
352
+ # Do NOT write projects.json
353
+ # Mock scanner to return results
354
+ # expect { scanner.scan_single(brand_key) }.to raise_error(Appydave::Tools::Dam::ConfigurationError, /Manifest not found/)
355
+ end
356
+
357
+ context 'when S3 returns empty results' do
358
+ # Mock scanner.scan_all_projects to return {}
359
+ # Expect: early return (no raise, no manifest write)
360
+ end
361
+
362
+ context 'when S3 has orphaned projects' do
363
+ # projects.json has ['b65-test'], S3 returns { 'b65-test' => ..., 'b99-orphan' => ... }
364
+ # Expect: orphaned_projects contains 'b99-orphan', matched_projects contains 'b65-test'
365
+ end
366
+ end
367
+
368
+ describe '#scan_all' do
369
+ context 'when one brand fails' do
370
+ # brands_config.brands returns [appydave, voz]
371
+ # scan_single raises for voz
372
+ # Expect: results array has appydave success: true, voz success: false
373
+ # Expect: no re-raise (scan_all rescues per-brand)
374
+ end
375
+ end
376
+ end
377
+ ```
378
+
379
+ **Fixture pattern for projects.json:**
380
+ ```ruby
381
+ manifest = {
382
+ config: { last_updated: Time.now.utc.iso8601, note: 'test' },
383
+ projects: [{ id: 'b65-test-project', storage: { s3: {} } }]
384
+ }
385
+ File.write(File.join(brand_path, 'projects.json'), JSON.generate(manifest))
386
+ ```
387
+
388
+ ### Done when B036 is complete
389
+ - `spec/appydave/tools/dam/s3_scan_command_spec.rb` has 8+ real behaviour examples
390
+ - `respond_to` smoke tests removed or replaced
391
+ - `RUBYOPT="-W0" bundle exec rspec` → 855+ examples, 0 failures
392
+ - `bundle exec rubocop --format clang` → 0 offenses
393
+ - `git status` clean before `kfix`
394
+
395
+ ---
396
+
397
+ ## B037: tests-local-sync-status
398
+
399
+ **File touched:** `spec/appydave/tools/dam/local_sync_status_spec.rb`
400
+
401
+ **No prerequisites** — but run after B035 for clean baseline.
402
+
403
+ ### Gaps to fill
404
+
405
+ **1. `:partial` case in `#enrich!`**
406
+
407
+ Set up `s3-staging` folder with 2 files but `file_count: 3` in S3 data:
408
+ ```ruby
409
+ context 'when local s3-staging has fewer files than S3' do
410
+ before do
411
+ FileUtils.mkdir_p(File.join(appydave_path, 'b65-test', 's3-staging'))
412
+ FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4'))
413
+ FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file2.mp4'))
414
+ end
415
+
416
+ it 'sets status to :partial' do
417
+ matched = { 'b65-test' => { file_count: 3 } }
418
+ described_class.enrich!(matched, 'appydave')
419
+ expect(matched['b65-test'][:local_status]).to eq(:partial)
420
+ end
421
+
422
+ it 'sets local_file_count to 2' do
423
+ matched = { 'b65-test' => { file_count: 3 } }
424
+ described_class.enrich!(matched, 'appydave')
425
+ expect(matched['b65-test'][:local_file_count]).to eq(2)
426
+ end
427
+ end
428
+ ```
429
+
430
+ **2. `local_file_count` assertion in existing `:synced` test**
431
+
432
+ Find the existing synced context and add:
433
+ ```ruby
434
+ it 'sets local_file_count correctly' do
435
+ expect(data[:local_file_count]).to eq(3) # adjust to match your fixture
436
+ end
437
+ ```
438
+
439
+ **3. Zone.Identifier exclusion**
440
+
441
+ Windows NTFS streams appear as `filename:Zone.Identifier` in samba mounts. These must NOT count as local files:
442
+ ```ruby
443
+ context 'when s3-staging contains Zone.Identifier files' do
444
+ before do
445
+ FileUtils.mkdir_p(File.join(appydave_path, 'b65-test', 's3-staging'))
446
+ FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4'))
447
+ FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4:Zone.Identifier'))
448
+ FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file2.mp4'))
449
+ end
450
+
451
+ it 'excludes Zone.Identifier files from local_file_count' do
452
+ matched = { 'b65-test' => { file_count: 2 } }
453
+ described_class.enrich!(matched, 'appydave')
454
+ expect(matched['b65-test'][:local_file_count]).to eq(2)
455
+ expect(matched['b65-test'][:local_status]).to eq(:synced)
456
+ end
457
+ end
458
+ ```
459
+
460
+ **4. Unknown status guard in `#format`**
461
+
462
+ ```ruby
463
+ describe '.format' do
464
+ it 'returns Unknown for unrecognised status' do
465
+ expect(described_class.format(:unknown, nil, 0)).to eq('Unknown')
466
+ end
467
+ end
468
+ ```
469
+
470
+ Check the actual `format` implementation first — if it already has an `else` branch returning `'Unknown'`, this is a documentation test. If it has no `else`, add one to `local_sync_status.rb` before writing this spec.
471
+
472
+ ### Done when B037 is complete
473
+ - `:partial` case tested with `local_file_count` assertion
474
+ - `:synced` case has `local_file_count` assertion
475
+ - `Zone.Identifier` exclusion tested
476
+ - `format(:unknown, ...)` tested
477
+ - `RUBYOPT="-W0" bundle exec rspec` → 851+ examples, 0 failures
478
+ - `bundle exec rubocop --format clang` → 0 offenses
479
+ - `git status` clean before `kfix`
480
+
481
+ ---
482
+
483
+ ## Success Criteria (Every Work Unit)
484
+
485
+ Every work unit must satisfy ALL of the following before marking `[x]`:
486
+
487
+ - [ ] `RUBYOPT="-W0" bundle exec rspec` — 847+ examples, 0 failures (rising with each WU)
488
+ - [ ] `bundle exec rubocop --format clang` — 0 offenses
489
+ - [ ] Line coverage stays ≥ 85.92%
490
+ - [ ] Any new `.rb` files start with `# frozen_string_literal: true`
491
+ - [ ] `git status` confirmed clean before `kfix`
492
+
493
+ ---
494
+
495
+ ## Reference Patterns
496
+
497
+ ### Shared Context for DAM Specs — THE STANDARD PATTERN
498
+
499
+ ```ruby
500
+ # spec/appydave/tools/dam/some_class_spec.rb
501
+ # frozen_string_literal: true
502
+
503
+ require 'spec_helper'
504
+
505
+ RSpec.describe Appydave::Tools::Dam::SomeClass do
506
+ include_context 'with vat filesystem and brands', brands: %w[appydave voz]
507
+
508
+ before do
509
+ FileUtils.mkdir_p(File.join(appydave_path, 'b65-test-project'))
510
+ end
511
+
512
+ describe '.some_method' do
513
+ it 'does the thing' do
514
+ expect(described_class.some_method('appydave')).to eq('expected')
515
+ end
516
+ end
517
+ end
518
+ ```
519
+
520
+ **Available from shared context:**
521
+ - `temp_folder` — temp root (auto-cleaned after each example)
522
+ - `projects_root` — `/path/to/temp/video-projects`
523
+ - `appydave_path`, `voz_path`, etc. — brand dirs (created on demand)
524
+ - `SettingsConfig#video_projects_root` is mocked to return `projects_root`
525
+
526
+ ### FileHelper — Use These, Don't Duplicate
527
+
528
+ ```ruby
529
+ Appydave::Tools::Dam::FileHelper.format_size(bytes) # "1.5 GB"
530
+ Appydave::Tools::Dam::FileHelper.calculate_directory_size(path) # Integer bytes
531
+ Appydave::Tools::Dam::FileHelper.format_age(time) # "3d", "2w", etc.
532
+ ```
533
+
534
+ ### BrandResolver — All Brand Name Transformations
535
+
536
+ ```ruby
537
+ BrandResolver.expand('appydave') # => 'v-appydave'
538
+ BrandResolver.expand('ad') # => 'v-appydave' (shortcut)
539
+ BrandResolver.normalize('v-appydave') # => 'appydave'
540
+ BrandResolver.validate('appydave') # => 'appydave' or raises BrandNotFoundError
541
+ ```
542
+
543
+ ### Typed Exception Pattern
544
+
545
+ ```ruby
546
+ raise Appydave::Tools::Dam::ConfigurationError, 'Manifest not found: /path'
547
+ raise Appydave::Tools::Dam::UsageError, 'Usage: dam s3-up <brand> <project>'
548
+ raise ProjectNotFoundError, 'Project name is required' if project_hint.nil?
549
+ raise BrandNotFoundError.new(brand, available_brands, fuzzy_suggestions)
550
+ ```
551
+
552
+ ### Config.brand_path — Correct Call
553
+
554
+ ```ruby
555
+ # brand_key is the short form ('appydave'), brand is expanded ('v-appydave')
556
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
557
+ # OR:
558
+ brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
559
+ # Both work — Config.brand_path resolves via BrandResolver internally
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Anti-Patterns to Avoid
565
+
566
+ - ❌ `exit 1` in library code — use typed exceptions from `errors.rb` instead
567
+ - ❌ `ENV['BRAND_PATH'] = ...` in library code — return `brand_path:` in result hash; set ENV in VatCLI
568
+ - ❌ Inline brand transformations — never write `"v-#{brand}"` outside BrandResolver
569
+ - ❌ `format_bytes` — does not exist; use `FileHelper.format_size`
570
+ - ❌ Mocking Config class methods in DAM specs — use shared filesystem context instead
571
+ - ❌ Multiple `before` blocks in same RSpec context — merge them (triggers RSpec/ScatteredSetup)
572
+ - ❌ `$?` for subprocess status — use `$CHILD_STATUS` (rubocop Style/SpecialGlobalVars)
573
+ - ❌ `raise 'string error'` in DAM module — use typed exceptions from `errors.rb`
574
+ - ❌ `include FileUtils` — use dam's `FileHelper` instead
575
+ - ❌ Hardcoded header strings for table output — always use `format()` matching data row format
576
+ - ❌ Adding new `Config.configure` calls — memoized but called redundantly; don't spread further
577
+
578
+ ---
579
+
580
+ ## Mock Patterns
581
+
582
+ ### ENV Stubbing (if needed in specs)
583
+
584
+ ```ruby
585
+ allow(ENV).to receive(:[]).and_call_original
586
+ allow(ENV).to receive(:[]).with('BRAND_PATH').and_return('/tmp/test/v-appydave')
587
+ ```
588
+
589
+ **Do NOT use climate_control gem** — project doesn't have it.
590
+
591
+ ### S3Scanner Mocking (for B036)
592
+
593
+ ```ruby
594
+ let(:mock_s3_scanner) { instance_double(Appydave::Tools::Dam::S3Scanner) }
595
+
596
+ before do
597
+ allow(Appydave::Tools::Dam::S3Scanner).to receive(:new).with(brand_key).and_return(mock_s3_scanner)
598
+ allow(mock_s3_scanner).to receive(:scan_all_projects).and_return({
599
+ 'b65-test-project' => { file_count: 3, total_bytes: 1_500_000, last_modified: '2025-01-01T00:00:00Z' }
600
+ })
601
+ end
602
+ ```
603
+
604
+ ### Config::Brands Mocking (for B036 scan_all)
605
+
606
+ ```ruby
607
+ let(:mock_brands_config) { instance_double(Appydave::Tools::Configuration::BrandsConfig) }
608
+ let(:mock_brand_info) { instance_double(Appydave::Tools::Configuration::BrandInfo, key: 'appydave') }
609
+
610
+ before do
611
+ allow(mock_brands_config).to receive(:brands).and_return([mock_brand_info])
612
+ end
613
+ ```
614
+
615
+ ---
616
+
617
+ ## Quality Gates
618
+
619
+ - **Tests:** `RUBYOPT="-W0" bundle exec rspec` — 847+ examples, 0 failures
620
+ - **Lint:** `bundle exec rubocop --format clang` — 0 offenses (CI will reject)
621
+ - **Coverage:** ≥ 85.92% line coverage
622
+ - **frozen_string_literal:** Required on every new `.rb` file
623
+ - **Commit format:** `kfeat`/`kfix` only — triggers semantic versioning + CI wait
624
+ - **Pre-commit:** Always run `git status` before `kfix` — confirm staged files
625
+
626
+ ---
627
+
628
+ ## Learnings
629
+
630
+ ### From DAM Enhancement Sprint (Jan 2025)
631
+
632
+ - **BrandResolver is the critical path.** All `dam` commands flow through it. Any change to brand resolution must be tested with all shortcuts.
633
+ - **`Regexp.last_match` is reset by `.sub()` calls.** Always capture regex groups BEFORE any string transformation.
634
+ - **`Config.configure` is memoized but called redundantly.** Don't add new calls.
635
+ - **Table format() pattern is non-obvious.** Headers misaligned 3 times in UAT. Always verify with real data.
636
+
637
+ ### From micro-cleanup (2026-03-19)
638
+
639
+ - **Dirty working tree + kfix = accidental staging.** Always run `git status` before committing.
640
+ - **Pre-existing "already fixed" items:** Check B-items aren't already done before acting (B031, B033, B015, B019 were all already committed).
641
+
642
+ ### From Architectural Review (2026-03-19)
643
+
644
+ - **bin/dam is the primary DAM CLI entry point.** Regressions here affect real workflows. Test every command path after extraction.
645
+ - **`ENV['BRAND_PATH']` is set in 5 places in bin/dam.** Three are in parse methods (being extracted to B035). Two remain in `generate_single_manifest` and `sync_ssd_command` — out of scope for this campaign.
646
+ - **Do NOT attempt B020 (split S3Operations) in this campaign.** Different class, different risk profile.
647
+
648
+ ### From extract-vat-cli (2026-03-19)
649
+
650
+ - **valid_brand? needs Config.brands mock.** The shared filesystem context only mocks `SettingsConfig`. When testing code that calls `Config.brands`, mock it separately: `allow(Appydave::Tools::Configuration::Config).to receive(:brands).and_return(...)`.
651
+ - **rubocop-disable directives become redundant when methods move.** After extracting to a library class, check the VatCLI file for orphaned disable/enable pairs and remove them.
652
+ - **VatCLI rescue blocks already catch StandardError.** No changes to VatCLI rescue needed when replacing `exit 1` with exceptions — the existing rescue chain handles it.
653
+ - **S3ScanCommand#scan_all already rescues per-brand.** The `rescue StandardError` inside `scan_all` means per-brand failures are isolated. Once `scan_single` raises instead of calling `exit`, `scan_all` catches it correctly — no structural change needed.
@@ -0,0 +1,61 @@
1
+ # IMPLEMENTATION_PLAN.md — library-boundary-cleanup
2
+
3
+ **Goal**: Fix two architectural boundary violations introduced by extract-vat-cli, then fill the test gaps they blocked. Prerequisite for B007 (parallelism) and clean library boundaries.
4
+ **Started**: 2026-03-19
5
+ **Target**: 4 items complete; 847+ examples passing; rubocop 0 offenses; no regressions
6
+
7
+ ## Summary
8
+ - Total: 4 | Complete: 0 | In Progress: 1 | Pending: 3 | Failed: 0
9
+
10
+ ## Pending
11
+ - [ ] B035 — extract-env-side-effect — Remove `ENV['BRAND_PATH']` side-effect from S3ArgParser; return `brand_path:` in result hash; update VatCLI callers (DEPENDS ON B034)
12
+ - [ ] B036 — tests-s3-scan-command — Rebuild S3ScanCommand spec from D-grade to B; 8–10 examples covering happy path, manifest missing, empty results, orphaned projects (DEPENDS ON B034)
13
+ - [ ] B037 — tests-local-sync-status — Add :partial case, local_file_count assertion, Zone.Identifier exclusion, unknown format guard
14
+
15
+ ## In Progress
16
+ - [~] B034 — extract-exit-calls — Replace `exit 1` with typed exceptions in S3ScanCommand + S3ArgParser; add `UsageError` to errors.rb
17
+
18
+ ## Complete
19
+
20
+ ## Failed / Needs Retry
21
+
22
+ ## Notes & Decisions
23
+
24
+ ### Wave Plan
25
+
26
+ **Wave 1 — B034** (alone)
27
+ - Touches: `s3_scan_command.rb`, `s3_arg_parser.rb`, `errors.rb`
28
+ - No parallel candidates — all three files must be edited together
29
+
30
+ **Wave 2 — B035** (after B034)
31
+ - Touches: `s3_arg_parser.rb`, `bin/dam`
32
+ - Cannot run with B034 (shared `s3_arg_parser.rb`)
33
+ - No behaviour change — moves ENV side-effect to CLI layer only
34
+
35
+ **Wave 3 — B036 + B037** (parallel — different spec files, no shared edits)
36
+ - B036: `spec/appydave/tools/dam/s3_scan_command_spec.rb`
37
+ - B037: `spec/appydave/tools/dam/local_sync_status_spec.rb`
38
+ - Both depend on B034 being complete (exceptions must exist before testing them)
39
+
40
+ ### Sequencing Constraints
41
+ - B034 must complete before B035 (both touch s3_arg_parser.rb)
42
+ - B034 must complete before B036 (can't test raised exceptions until they exist)
43
+ - B035 must complete before B036 + B037 (ENV side-effect removal may affect test setup)
44
+ - B036 and B037 are fully parallel once B034 + B035 are done
45
+
46
+ ### Exit Locations Confirmed (read 2026-03-19)
47
+ - `S3ScanCommand#scan_single` line 55: manifest not found → raise `ConfigurationError`
48
+ - `S3ArgParser#parse_s3` line 25: PWD auto-detect fail → raise `UsageError`
49
+ - `S3ArgParser#parse_s3` line 43: invalid brand → raise `UsageError`
50
+ - `S3ArgParser#parse_discover` line 100: missing brand/project args → raise `UsageError`
51
+ - `S3ArgParser#show_share_usage_and_exit` line 131: missing share args → raise `UsageError`
52
+
53
+ ### ENV Side-Effect Locations Confirmed (read 2026-03-19)
54
+ - `S3ArgParser#parse_s3` line 51: `ENV['BRAND_PATH'] = ...`
55
+ - `S3ArgParser#parse_share` line 82: `ENV['BRAND_PATH'] = ...`
56
+ - `S3ArgParser#parse_discover` line 108: `ENV['BRAND_PATH'] = ...`
57
+ - Fix: return `brand_path:` in each result hash; update VatCLI callers to set ENV there
58
+
59
+ ### VatCLI Rescue Blocks
60
+ - `s3_scan_command`, `s3_up_command`, etc. already rescue `StandardError => e` and `puts "❌ Error: #{e.message}"`
61
+ - No VatCLI rescue changes needed for B034 — existing rescues already catch DamError (which inherits from StandardError)
@@ -34,6 +34,9 @@ module Appydave
34
34
 
35
35
  # Raised when git operation fails
36
36
  class GitOperationError < DamError; end
37
+
38
+ # Raised when CLI arguments are invalid or missing
39
+ class UsageError < DamError; end
37
40
  end
38
41
  end
39
42
  end
@@ -20,26 +20,15 @@ module Appydave
20
20
  # Auto-detect from PWD
21
21
  brand, project_id = Appydave::Tools::Dam::ProjectResolver.detect_from_pwd
22
22
  if brand.nil? || project_id.nil?
23
- puts '❌ Could not auto-detect brand/project from current directory'
24
- puts "Usage: dam #{command} <brand> <project> [--dry-run]"
25
- exit 1
23
+ raise Appydave::Tools::Dam::UsageError,
24
+ "Could not auto-detect brand/project from current directory. Usage: dam #{command} <brand> <project> [--dry-run]"
26
25
  end
27
26
  brand_key = brand # Already detected, use as-is
28
27
  else
29
28
  # Validate brand exists before trying to resolve project
30
29
  unless valid_brand?(brand_arg)
31
- puts "❌ Invalid brand: '#{brand_arg}'"
32
- puts ''
33
- puts 'Valid brands:'
34
- puts ' appydave → v-appydave (AppyDave brand)'
35
- puts ' voz → v-voz (VOZ client)'
36
- puts ' aitldr → v-aitldr (AITLDR brand)'
37
- puts ' kiros → v-kiros (Kiros client)'
38
- puts ' joy → v-beauty-and-joy (Beauty & Joy)'
39
- puts ' ss → v-supportsignal (SupportSignal)'
40
- puts ''
41
- puts "Usage: dam #{command} <brand> <project> [--dry-run]"
42
- exit 1
30
+ raise Appydave::Tools::Dam::UsageError,
31
+ "Invalid brand: '#{brand_arg}'. Valid brands: appydave, voz, aitldr, kiros, joy, ss. Usage: dam #{command} <brand> <project> [--dry-run]"
43
32
  end
44
33
 
45
34
  brand_key = brand_arg # Use the shortcut/key (e.g., 'appydave')
@@ -72,7 +61,7 @@ module Appydave
72
61
  project_arg = args[1]
73
62
  file_arg = args[2]
74
63
 
75
- show_share_usage_and_exit if brand_arg.nil? || project_arg.nil? || file_arg.nil?
64
+ raise_share_usage_error if brand_arg.nil? || project_arg.nil? || file_arg.nil?
76
65
 
77
66
  brand_key = brand_arg
78
67
  brand = Appydave::Tools::Dam::Config.expand_brand(brand_arg)
@@ -92,12 +81,8 @@ module Appydave
92
81
  project_arg = args[1]
93
82
 
94
83
  if brand_arg.nil? || project_arg.nil?
95
- puts 'Usage: dam s3-discover <brand> <project> [--shareable]'
96
- puts ''
97
- puts 'Examples:'
98
- puts ' dam s3-discover appydave b70 # List files'
99
- puts ' dam s3-discover appydave b70 --shareable # Generate share commands'
100
- exit 1
84
+ raise Appydave::Tools::Dam::UsageError,
85
+ 'Usage: dam s3-discover <brand> <project> [--shareable]'
101
86
  end
102
87
 
103
88
  brand_key = brand_arg
@@ -116,19 +101,9 @@ module Appydave
116
101
  brands.key?(brand_key) || brands.shortcut?(brand_key)
117
102
  end
118
103
 
119
- def show_share_usage_and_exit
120
- puts 'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
121
- puts ''
122
- puts 'Options:'
123
- puts ' --expires TIME Expiry time (default: 7d)'
124
- puts ' --download Force download instead of viewing in browser'
125
- puts ''
126
- puts 'Examples:'
127
- puts ' dam s3-share appydave b70 video.mp4'
128
- puts ' dam s3-share appydave b70 video.mp4 --expires 24h'
129
- puts ' dam s3-share appydave b70 video.mp4 --download'
130
- puts ' dam s3-share voz boy-baker final-edit.mov --expires 3d --download'
131
- exit 1
104
+ def raise_share_usage_error
105
+ raise Appydave::Tools::Dam::UsageError,
106
+ 'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
132
107
  end
133
108
  end
134
109
  end
@@ -49,10 +49,8 @@ module Appydave
49
49
  manifest_path = File.join(brand_path, 'projects.json')
50
50
 
51
51
  unless File.exist?(manifest_path)
52
- puts "❌ Manifest not found: #{manifest_path}"
53
- puts " Run: dam manifest #{brand_key}"
54
- puts " Then retry: dam s3-scan #{brand_key}"
55
- exit 1
52
+ raise Appydave::Tools::Dam::ConfigurationError,
53
+ "Manifest not found: #{manifest_path}. Run: dam manifest #{brand_key}"
56
54
  end
57
55
 
58
56
  manifest = JSON.parse(File.read(manifest_path), symbolize_names: true)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.76.7'
5
+ VERSION = '0.76.8'
6
6
  end
7
7
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.76.7",
3
+ "version": "0.76.8",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.76.7
4
+ version: 0.76.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
@@ -302,12 +302,16 @@ files:
302
302
  - docs/planning/bugfix-and-security/assessment.md
303
303
  - docs/planning/extract-vat-cli/AGENTS.md
304
304
  - docs/planning/extract-vat-cli/IMPLEMENTATION_PLAN.md
305
+ - docs/planning/extract-vat-cli/assessment.md
306
+ - docs/planning/extract-vat-cli/learnings/wave-learnings.md
305
307
  - docs/planning/final-test-gaps/AGENTS.md
306
308
  - docs/planning/final-test-gaps/IMPLEMENTATION_PLAN.md
307
309
  - docs/planning/final-test-gaps/assessment.md
308
310
  - docs/planning/fr2-gpt-context-help/AGENTS.md
309
311
  - docs/planning/fr2-gpt-context-help/IMPLEMENTATION_PLAN.md
310
312
  - docs/planning/fr2-gpt-context-help/assessment.md
313
+ - docs/planning/library-boundary-cleanup/AGENTS.md
314
+ - docs/planning/library-boundary-cleanup/IMPLEMENTATION_PLAN.md
311
315
  - docs/planning/micro-cleanup/AGENTS.md
312
316
  - docs/planning/micro-cleanup/IMPLEMENTATION_PLAN.md
313
317
  - docs/planning/micro-cleanup/assessment.md