toilet_tracker 0.1.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.
data/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # ToiletTracker
2
+
3
+ A modern Ruby gem for parsing WhatsApp messages containing toilet tracking data using emoji-based commands. Built with clean architecture principles and comprehensive timezone support.
4
+
5
+ ## Features
6
+
7
+ - ๐Ÿšฝ **Settings**: Set timezone shifts and preferences
8
+ - ๐Ÿ’ฉ **Event Tracking**: Log events with automatic timezone adjustments
9
+ - โฐ **Timezone Support**: Handle multiple timezones and retroactive shifts
10
+ - ๐Ÿ“Š **Multiple Output Formats**: JSON, CSV, and formatted tables
11
+ - ๐Ÿ”ง **Thor CLI**: Interactive command-line interface with TTY::Prompt
12
+ - ๐Ÿงช **Comprehensive Testing**: 77 tests with 96%+ coverage
13
+ - ๐Ÿ—๏ธ **Clean Architecture**: Modular design with clear separation of concerns
14
+ - ๐Ÿ“‹ **Statistics**: Detailed parsing statistics and error reporting
15
+
16
+ ## Requirements
17
+
18
+ - **Ruby 3.4+** - Uses modern Ruby features for optimal performance
19
+ - **StandardRB** - Zero-configuration linting and formatting
20
+
21
+ ## Installation
22
+
23
+ Add this line to your application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'toilet_tracker'
27
+ ```
28
+
29
+ And then execute:
30
+
31
+ ```bash
32
+ bundle install
33
+ ```
34
+
35
+ Or install it yourself as:
36
+
37
+ ```bash
38
+ gem install toilet_tracker
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ### Command Line Interface
44
+
45
+ Process WhatsApp export files:
46
+
47
+ ```bash
48
+ # Basic usage
49
+ toilet_tracker parse whatsapp_export.txt
50
+
51
+ # Parse WhatsApp zip exports (automatically extracts _chat.txt)
52
+ toilet_tracker parse whatsapp_export.zip
53
+
54
+ # JSON output
55
+ toilet_tracker parse --format json messages.txt
56
+
57
+ # Custom timezone
58
+ toilet_tracker parse --timezone "Europe/Rome" messages.txt
59
+
60
+ # Show statistics
61
+ toilet_tracker stats messages.txt
62
+
63
+ # Interactive mode
64
+ toilet_tracker interactive
65
+
66
+ # Show version
67
+ toilet_tracker version
68
+ ```
69
+
70
+ ### Ruby API
71
+
72
+ ```ruby
73
+ require 'toilet_tracker'
74
+
75
+ # Parse individual lines
76
+ result = ToiletTracker.parse_line("[01/01/25, 12:00:00] Alice: ๐Ÿ’ฉ")
77
+ puts result.type # => :event_now
78
+
79
+ # Parse multiple lines
80
+ lines = [
81
+ "[01/01/25, 10:00:00] Alice: ๐Ÿšฝ +2",
82
+ "[01/01/25, 12:00:00] Alice: ๐Ÿ’ฉ"
83
+ ]
84
+ results = ToiletTracker.parse_lines(lines)
85
+
86
+ # Configure settings
87
+ ToiletTracker.configure do |config|
88
+ config.default_timezone = "Europe/Rome"
89
+ config.max_shift_hours = 12
90
+ end
91
+ ```
92
+
93
+ ### Supported Message Formats
94
+
95
+ #### ๐Ÿšฝ Settings (Functional - Affect Timestamps)
96
+ - `๐Ÿšฝ +2` - Set timezone shift to +2 hours (immediate)
97
+ - `๐Ÿšฝ -1` - Set timezone shift to -1 hour (immediate)
98
+ - `๐Ÿšฝ +1 2025-01-15 14:30` - Set shift effective from specific time (retroactive)
99
+ - `๐Ÿšฝ Europe/Rome` - Set timezone by name
100
+ - `๐Ÿšฝ PST` - Set timezone by abbreviation
101
+
102
+ #### ๐Ÿ’ฉ Events
103
+
104
+ **Functional Shifts (Affect Actual Times):**
105
+ - `๐Ÿ’ฉ` - Log live event with current active shift
106
+ - `๐Ÿ’ฉ +1` - Log live event with +1 hour shift override
107
+ - `๐Ÿ’ฉ PST` - Log live event converted to PST timezone
108
+
109
+ **Fixed Times (No Shift Applied):**
110
+ - `๐Ÿ’ฉ 2025-01-15 14:30` - Log event at exact time specified
111
+
112
+ **Decorative Data (For Statistics Only):**
113
+ - `๐Ÿ’ฉ +2 2025-01-15 14:30` - Log event at 14:30 exactly, +2 shift stored for display
114
+ - `๐Ÿ’ฉ EST 2025-01-15 14:30` - Log event at 14:30 exactly, EST timezone stored for display
115
+
116
+ > **Important**: In decorative patterns, the shift/timezone is stored for statistics but does NOT modify the actual poop timestamp.
117
+
118
+ ### Configuration Options
119
+
120
+ ```ruby
121
+ ToiletTracker.configure do |config|
122
+ # Default timezone for parsing timestamps
123
+ config.default_timezone = "UTC"
124
+
125
+ # Maximum allowed shift hours
126
+ config.max_shift_hours = 12
127
+
128
+ # Custom message patterns (advanced)
129
+ config.message_patterns[:custom] = /custom_pattern/
130
+
131
+ # Supported date formats
132
+ config.date_formats = ["%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M"]
133
+ end
134
+ ```
135
+
136
+ ## Examples
137
+
138
+ ### Basic Usage Scenarios
139
+
140
+ #### Setting Up Timezone Shifts
141
+ ```
142
+ [15/01/25, 10:00:00] Alice: ๐Ÿšฝ +2
143
+ [15/01/25, 12:00:00] Alice: ๐Ÿ’ฉ
144
+ # Result: Event logged at 14:00 (12:00 + 2 hours)
145
+ ```
146
+
147
+ #### Retroactive Timezone Adjustments
148
+ ```
149
+ [15/01/25, 10:00:00] Alice: ๐Ÿ’ฉ
150
+ [15/01/25, 14:00:00] Alice: ๐Ÿšฝ +3 2025-01-15 09:00:00
151
+ # Result: First event adjusted to 13:00 (10:00 + 3 hours)
152
+ ```
153
+
154
+ #### Decorative vs Functional Shifts
155
+ ```
156
+ # Functional: Affects timestamp
157
+ [15/01/25, 12:00:00] Alice: ๐Ÿ’ฉ +2
158
+ # Result: Event at 14:00 (12:00 + 2 hours)
159
+
160
+ # Decorative: Only for statistics
161
+ [15/01/25, 12:00:00] Alice: ๐Ÿ’ฉ +2 2025-01-15 10:00:00
162
+ # Result: Event at exactly 10:00, +2 stored for display
163
+ ```
164
+
165
+ #### Complex Multi-User Scenario
166
+ ```
167
+ [15/01/25, 09:00:00] Alice: ๐Ÿšฝ +2
168
+ [15/01/25, 09:30:00] Bob: ๐Ÿšฝ Europe/Rome
169
+ [15/01/25, 10:00:00] Alice: ๐Ÿ’ฉ
170
+ [15/01/25, 10:30:00] Bob: ๐Ÿ’ฉ
171
+ [15/01/25, 11:00:00] Alice: ๐Ÿ’ฉ +1
172
+ [15/01/25, 11:30:00] Bob: ๐Ÿ’ฉ EST 2025-01-15 08:00:00
173
+ ```
174
+
175
+ Results:
176
+ - Alice's first event: 12:00 (10:00 + 2 hours active shift)
177
+ - Bob's first event: ~11:30 (10:30 converted from UTC to Rome time)
178
+ - Alice's override event: 12:00 (11:00 + 1 hour override)
179
+ - Bob's decorative event: exactly 08:00 (EST is decorative only)
180
+
181
+ ## Development
182
+
183
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run the following commands:
184
+
185
+ ```bash
186
+ # Run tests
187
+ bundle exec rspec
188
+
189
+ # Check code style (StandardRB)
190
+ bundle exec standardrb
191
+
192
+ # Auto-fix style issues
193
+ bundle exec standardrb --fix
194
+
195
+ # Install gem locally
196
+ bundle exec rake install
197
+ ```
198
+
199
+ ### Code Quality
200
+
201
+ This project uses **StandardRB** for zero-configuration Ruby linting and formatting. StandardRB eliminates style debates by providing an opinionated, unconfigurable set of rules.
202
+
203
+ Key principles:
204
+ - **Ruby 3.4+ features** - Modern syntax and performance optimizations
205
+ - **KISS principle** - Keep code simple and readable
206
+ - **Test-driven development** - Comprehensive RSpec test suite
207
+ - **Clean architecture** - Clear separation of concerns
208
+
209
+ ### Creating a Release
210
+
211
+ To release a new version:
212
+
213
+ 1. **Update the version** in `lib/toilet_tracker/version.rb`:
214
+ ```ruby
215
+ module ToiletTracker
216
+ VERSION = "1.2.3"
217
+ end
218
+ ```
219
+
220
+ 2. **Update CHANGELOG.md** with release notes (optional but recommended)
221
+
222
+ 3. **Commit your changes**:
223
+ ```bash
224
+ git add -A
225
+ git commit -m "Bump version to 1.2.3"
226
+ ```
227
+
228
+ 4. **Create and push a git tag**:
229
+ ```bash
230
+ git tag v1.2.3
231
+ git push origin main --tags
232
+ ```
233
+
234
+ 5. **Automated Release Process**: The GitHub Actions workflow will automatically:
235
+ - Run tests and linting to validate the release
236
+ - Build the gem
237
+ - Publish to RubyGems.org (requires `RUBYGEMS_API_KEY` secret)
238
+ - Create a GitHub release with auto-generated changelog
239
+ - Upload the gem file as a release asset
240
+
241
+ **Note**: Ensure the `RUBYGEMS_API_KEY` secret is configured in your GitHub repository settings for automatic publishing to work.
242
+
243
+ ## Contributing
244
+
245
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/toilet_tracker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/toilet_tracker/blob/main/CODE_OF_CONDUCT.md).
246
+
247
+ ## License
248
+
249
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
250
+
251
+ ## Code of Conduct
252
+
253
+ Everyone interacting in the ToiletTracker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/toilet_tracker/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/STYLE_GUIDE.md ADDED
@@ -0,0 +1,377 @@
1
+ # ToiletTracker Ruby Style Guide
2
+
3
+ ## Overview
4
+
5
+ This project follows Ruby 3.4 best practices with StandardRB for consistent code formatting. The guide emphasizes KISS (Keep It Simple, Stupid) principles and modern Ruby idioms.
6
+
7
+ ## Ruby Version & Features
8
+
9
+ **Target Ruby Version**: 3.4+
10
+
11
+ ### Ruby 3.4 Modern Features
12
+
13
+ #### Use `it` for Single Block Parameters
14
+ ```ruby
15
+ # โœ… Preferred (Ruby 3.4+)
16
+ [1, 2, 3].map { it * 2 }
17
+ results.filter { it.success? }
18
+
19
+ # โŒ Avoid (outdated)
20
+ [1, 2, 3].map { _1 * 2 }
21
+ results.filter { _1.success? }
22
+ ```
23
+
24
+ #### Frozen Strings by Default
25
+ ```ruby
26
+ # โœ… Remove these comments (redundant in Ruby 3.4)
27
+ # frozen_string_literal: true
28
+
29
+ # Ruby 3.4 makes strings frozen by default
30
+ ```
31
+
32
+ #### Modern Hash Syntax
33
+ ```ruby
34
+ # โœ… Preferred - Symbol keys without fat arrow
35
+ user_data = { name: "Alice", age: 30, active: true }
36
+
37
+ # โœ… Other keys with spaces around fat arrow
38
+ mixed_data = { "user-id" => 123, :name => "Alice" }
39
+
40
+ # โŒ Avoid - Old symbol syntax
41
+ user_data = { :name => "Alice", :age => 30 }
42
+ ```
43
+
44
+ ## Code Style (StandardRB)
45
+
46
+ ### Linting & Formatting
47
+ ```bash
48
+ # Check code style
49
+ bundle exec standardrb
50
+
51
+ # Auto-fix issues
52
+ bundle exec standardrb --fix
53
+ ```
54
+
55
+ ### Configuration
56
+ StandardRB is **unconfigurable by design** to eliminate style debates. The `.standard.yml` file contains:
57
+ ```yaml
58
+ ruby_version: 3.4
59
+ fix: true
60
+ ignore:
61
+ - 'vendor/**/*'
62
+ ```
63
+
64
+ ### Event Naming Convention
65
+ We use clear, unified event names:
66
+ - **Settings**: `shift_set`, `shift_set_at_time`, `timezone_set`
67
+ - **Events**: `event_now`, `event_now_shifted`, `event_at_time`, `event_at_time_shifted`, `event_in_timezone`, `event_in_timezone_at_time`
68
+
69
+ This naming is consistent, self-explanatory, and avoids domain-specific jargon.
70
+
71
+ ## KISS Principles Applied
72
+
73
+ ### 1. Simple Method Design
74
+ ```ruby
75
+ # โœ… Good - Single responsibility, clear intent
76
+ def parse_shift(shift_string)
77
+ return nil if shift_string.nil? || shift_string.empty?
78
+
79
+ shift = Integer(shift_string)
80
+ validate_shift_range(shift)
81
+ shift
82
+ rescue ArgumentError
83
+ raise Core::InvalidShift, shift_string
84
+ end
85
+
86
+ # โŒ Avoid - Complex, multiple responsibilities
87
+ def parse_shift_and_maybe_validate_and_log(shift_string, should_log = false)
88
+ # ... complex logic mixing concerns
89
+ end
90
+ ```
91
+
92
+ ### 2. Clear Variable Naming
93
+ ```ruby
94
+ # โœ… Self-documenting names
95
+ effective_time = result.effective_time || result.message.timestamp
96
+ user_timezone_shift = shift_service.get_active_shift(user, timestamp)
97
+
98
+ # โŒ Unclear abbreviations
99
+ eff_t = result.eff_t || result.msg.ts
100
+ usr_tz_sh = shift_svc.get_act_sh(usr, ts)
101
+ ```
102
+
103
+ ### 3. Prefer Simple Data Structures
104
+ ```ruby
105
+ # โœ… Use Ruby 3+ Data classes for immutable structs
106
+ ParseResult = Data.define(:type, :status, :message, :error_details)
107
+
108
+ # โœ… Simple hash for configuration
109
+ config = {
110
+ default_timezone: "UTC",
111
+ max_shift_hours: 12
112
+ }
113
+
114
+ # โŒ Avoid over-engineered class hierarchies for simple data
115
+ ```
116
+
117
+ ### 4. Error Handling
118
+ ```ruby
119
+ # โœ… Explicit error types with clear messages
120
+ def parse_datetime(datetime_string)
121
+ # Try each format
122
+ config.date_formats.each do |format|
123
+ return parse_with_format(datetime_string, format)
124
+ rescue ArgumentError
125
+ next
126
+ end
127
+
128
+ raise Core::InvalidDateTime, "Unable to parse: #{datetime_string}"
129
+ end
130
+
131
+ # โŒ Generic error swallowing
132
+ def parse_datetime(datetime_string)
133
+ begin
134
+ # ... complex parsing
135
+ rescue => e
136
+ nil # Lost error context
137
+ end
138
+ end
139
+ ```
140
+
141
+ ## Method Organization
142
+
143
+ ### Size Limits
144
+ - **Methods**: Max 15 lines (StandardRB default)
145
+ - **Classes**: Max 100 lines for clarity
146
+ - **Files**: Single responsibility per file
147
+
148
+ ### Method Structure
149
+ ```ruby
150
+ def process_toilet_shift(result)
151
+ user = result.message.sender
152
+ effective_time = determine_effective_time(result)
153
+
154
+ if retroactive_shift?(result)
155
+ apply_retroactive_shift(user, result.shift, effective_time)
156
+ else
157
+ apply_immediate_shift(user, result.shift, effective_time)
158
+ end
159
+
160
+ result
161
+ rescue Core::Error => e
162
+ create_error_result(result, e)
163
+ end
164
+
165
+ private
166
+
167
+ def retroactive_shift?(result)
168
+ result.effective_time && result.effective_time < result.message.timestamp
169
+ end
170
+ ```
171
+
172
+ ## Testing Style
173
+
174
+ ### RSpec Conventions
175
+ ```ruby
176
+ # โœ… Clear, descriptive test names
177
+ RSpec.describe MessageParser do
178
+ describe "#parse" do
179
+ context "when parsing toilet shift commands" do
180
+ it "creates ToiletShiftResult for valid shift" do
181
+ message = build_message("๐Ÿšฝ +2")
182
+ result = parser.parse(message)
183
+
184
+ expect(result).to be_success
185
+ expect(result.value.shift).to eq(2)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ # โœ… Use subject and let for clarity
192
+ subject(:parser) { described_class.new }
193
+ let(:message) { build_message(content) }
194
+ let(:content) { "๐Ÿšฝ +2" }
195
+ ```
196
+
197
+ ### Test Organization
198
+ - One test file per class/module
199
+ - Group related tests with `describe` and `context`
200
+ - Use `let` for test data setup
201
+ - Keep tests focused on single behavior
202
+
203
+ ## Performance Considerations
204
+
205
+ ### Array and Hash Operations
206
+ ```ruby
207
+ # โœ… Leverage Ruby 3.4 performance improvements
208
+ large_array.each { process_item(it) } # 7x faster in Ruby 3.4
209
+ results.select { it.success? } # Optimized in Ruby 3.4
210
+
211
+ # โœ… Use appropriate collection methods
212
+ timezone_map = timezones.to_h { [it.name, it.offset] }
213
+ valid_results = results.filter_map { it if it.valid? }
214
+ ```
215
+
216
+ ### Regex Compilation
217
+ ```ruby
218
+ # โœ… Compile expensive regexes once
219
+ class Configuration
220
+ def whatsapp_message_pattern
221
+ @whatsapp_message_pattern ||= compile_whatsapp_pattern
222
+ end
223
+
224
+ private
225
+
226
+ def compile_whatsapp_pattern
227
+ # Complex regex compilation
228
+ end
229
+ end
230
+ ```
231
+
232
+ ## Documentation
233
+
234
+ ### Inline Comments
235
+ ```ruby
236
+ # โœ… Explain business logic, not obvious code
237
+ def determine_status(event_time, message_time)
238
+ return :error unless event_time && message_time
239
+
240
+ # Alert if the time difference is suspiciously large (>30 days)
241
+ # This catches potential data entry errors
242
+ time_diff = (message_time - event_time).abs
243
+ time_diff > 30.days ? :alert : :ok
244
+ end
245
+
246
+ # โŒ Don't comment obvious code
247
+ # Set user to the message sender
248
+ user = message.sender
249
+ ```
250
+
251
+ ### Method Documentation
252
+ ```ruby
253
+ # Document complex public APIs only
254
+ def analyze_lines(lines)
255
+ # Parse all lines first
256
+ results = lines.filter_map { analyze_line(it) }
257
+
258
+ # Apply retroactive shifts to adjust timestamps
259
+ apply_retroactive_shifts_to_results(results)
260
+ end
261
+ ```
262
+
263
+ ## File Organization
264
+
265
+ ### Directory Structure
266
+ ```
267
+ lib/
268
+ โ”œโ”€โ”€ toilet_tracker.rb # Main entry point & API
269
+ โ”œโ”€โ”€ toilet_tracker/
270
+ โ”‚ โ”œโ”€โ”€ version.rb # Gem version
271
+ โ”‚ โ”œโ”€โ”€ configuration.rb # Config & patterns
272
+ โ”‚ โ”œโ”€โ”€ core/ # Domain objects
273
+ โ”‚ โ”‚ โ”œโ”€โ”€ errors.rb # Custom exceptions
274
+ โ”‚ โ”‚ โ”œโ”€โ”€ message.rb # Message value object
275
+ โ”‚ โ”‚ โ”œโ”€โ”€ result.rb # Result monad
276
+ โ”‚ โ”‚ โ””โ”€โ”€ parse_result.rb # Data classes for results
277
+ โ”‚ โ”œโ”€โ”€ parsers/ # Parsing logic
278
+ โ”‚ โ”‚ โ”œโ”€โ”€ base_parser.rb # Shared behavior
279
+ โ”‚ โ”‚ โ”œโ”€โ”€ whatsapp_parser.rb # WhatsApp format
280
+ โ”‚ โ”‚ โ””โ”€โ”€ message_parser.rb # Content parsing
281
+ โ”‚ โ””โ”€โ”€ services/ # Business logic
282
+ โ”‚ โ”œโ”€โ”€ analysis_service.rb # Main orchestrator
283
+ โ”‚ โ”œโ”€โ”€ shift_service.rb # Timezone shifts
284
+ โ”‚ โ””โ”€โ”€ timezone_service.rb # Timezone handling
285
+
286
+ exe/
287
+ โ””โ”€โ”€ toilet_tracker # Thor CLI executable
288
+
289
+ spec/
290
+ โ”œโ”€โ”€ spec_helper.rb # Test configuration
291
+ โ”œโ”€โ”€ toilet_tracker_spec.rb # Main API tests
292
+ โ””โ”€โ”€ toilet_tracker/ # Organized test structure
293
+ โ”œโ”€โ”€ core/
294
+ โ”œโ”€โ”€ parsers/
295
+ โ””โ”€โ”€ services/
296
+ ```
297
+
298
+ ### Naming Conventions
299
+ - **Files**: `snake_case.rb`
300
+ - **Classes/Modules**: `PascalCase`
301
+ - **Methods/Variables**: `snake_case`
302
+ - **Constants**: `SCREAMING_SNAKE_CASE`
303
+
304
+ ## Dependencies
305
+
306
+ ### Runtime Dependencies
307
+ ```ruby
308
+ # Keep minimal, prefer standard library
309
+ gem "activesupport", "~> 8.0" # For TimeZone support only
310
+ gem "thor", "~> 1.3" # CLI framework
311
+ gem "tty-prompt", "~> 0.23" # Interactive CLI
312
+ gem "csv", "~> 3.3" # Ruby 3.4 compatibility
313
+ ```
314
+
315
+ ### Development Dependencies
316
+ ```ruby
317
+ gem "standard" # Zero-config linting
318
+ gem "rspec" # Testing framework
319
+ gem "simplecov" # Code coverage
320
+ gem "rake" # Task runner
321
+ ```
322
+
323
+ ## Anti-Patterns to Avoid
324
+
325
+ ### โŒ Over-Engineering
326
+ ```ruby
327
+ # Don't create complex inheritance hierarchies for simple data
328
+ class BaseResult < AbstractResult
329
+ include ResultBehaviors
330
+ extend ResultClassMethods
331
+ # ... 50 lines of abstract methods
332
+ end
333
+ ```
334
+
335
+ ### โŒ Magic Numbers
336
+ ```ruby
337
+ # Don't use unexplained constants
338
+ time_diff > 2592000 ? :alert : :ok # What is 2592000?
339
+
340
+ # โœ… Use named constants
341
+ time_diff > 30.days ? :alert : :ok
342
+ ```
343
+
344
+ ### โŒ Method Chaining Abuse
345
+ ```ruby
346
+ # Don't create unreadable chains
347
+ result.message.content.strip.downcase.gsub(/\s+/, " ").split.first&.upcase
348
+
349
+ # โœ… Break into readable steps
350
+ content = result.message.content
351
+ normalized_content = normalize_whitespace(content)
352
+ first_word = normalized_content.split.first
353
+ first_word&.upcase
354
+ ```
355
+
356
+ ## Tools Integration
357
+
358
+ ### Git Hooks (Overcommit)
359
+ ```yaml
360
+ # .overcommit.yml
361
+ PreCommit:
362
+ StandardRb:
363
+ enabled: true
364
+ command: ['bundle', 'exec', 'standardrb']
365
+ ```
366
+
367
+ ### CI/CD
368
+ ```yaml
369
+ # GitHub Actions
370
+ - name: Run StandardRB
371
+ run: bundle exec standardrb
372
+
373
+ - name: Run Tests
374
+ run: bundle exec rspec
375
+ ```
376
+
377
+ This style guide ensures ToiletTracker follows modern Ruby best practices while maintaining simplicity and readability.