njtransit 1.0.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/njtransit.md +196 -0
  3. data/.mcp.json.example +12 -0
  4. data/.mcp.json.sample +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +87 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +37 -0
  9. data/CLAUDE.md +159 -0
  10. data/CODE_OF_CONDUCT.md +84 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +148 -0
  13. data/Rakefile +12 -0
  14. data/docs/plans/2025-01-24-njtransit-gem-design.md +112 -0
  15. data/docs/plans/2026-01-24-bus-api-design.md +119 -0
  16. data/docs/plans/2026-01-24-gtfs-implementation.md +2216 -0
  17. data/docs/plans/2026-01-24-gtfs-loader-design.md +351 -0
  18. data/docs/superpowers/plans/2026-03-26-dev-infra-and-agent.md +480 -0
  19. data/lefthook.yml +17 -0
  20. data/lib/njtransit/client.rb +291 -0
  21. data/lib/njtransit/configuration.rb +49 -0
  22. data/lib/njtransit/error.rb +50 -0
  23. data/lib/njtransit/gtfs/database.rb +145 -0
  24. data/lib/njtransit/gtfs/importer.rb +124 -0
  25. data/lib/njtransit/gtfs/models/route.rb +59 -0
  26. data/lib/njtransit/gtfs/models/stop.rb +63 -0
  27. data/lib/njtransit/gtfs/queries/routes_between.rb +62 -0
  28. data/lib/njtransit/gtfs/queries/schedule.rb +75 -0
  29. data/lib/njtransit/gtfs.rb +119 -0
  30. data/lib/njtransit/railtie.rb +9 -0
  31. data/lib/njtransit/resources/base.rb +35 -0
  32. data/lib/njtransit/resources/bus/enrichment.rb +105 -0
  33. data/lib/njtransit/resources/bus.rb +95 -0
  34. data/lib/njtransit/resources/bus_gtfs.rb +34 -0
  35. data/lib/njtransit/resources/rail.rb +47 -0
  36. data/lib/njtransit/resources/rail_gtfs.rb +27 -0
  37. data/lib/njtransit/tasks.rb +74 -0
  38. data/lib/njtransit/version.rb +5 -0
  39. data/lib/njtransit.rb +40 -0
  40. data/sig/njtransit.rbs +4 -0
  41. metadata +177 -0
@@ -0,0 +1,351 @@
1
+ # GTFS Static Data Loader Design
2
+
3
+ **Date:** 2026-01-24
4
+ **Issue:** [#3 - Add GTFS static data loader for complete/deterministic dataset](https://github.com/jayrav13/njtransit/issues/3)
5
+ **Status:** Approved
6
+
7
+ ## Problem
8
+
9
+ The real-time Bus API has limitations that prevent building a complete dataset:
10
+
11
+ | Limitation | Impact |
12
+ |------------|--------|
13
+ | No lat/lon in `stops()` response | Can't plot stops on a map |
14
+ | `departures()` only shows ~1hr window | Miss routes with later trips |
15
+ | No route discovery | Can't find "all routes from A to B" |
16
+
17
+ GTFS static data solves these gaps and enables enriching real-time API responses with complete information.
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ ┌─────────────────────────────────────────────────────────┐
23
+ │ NJTransit Gem │
24
+ ├─────────────────────────────────────────────────────────┤
25
+ │ │
26
+ │ ┌─────────────┐ ┌─────────────────────────┐ │
27
+ │ │ Bus API │─────────▶│ GTFS (enrichment) │ │
28
+ │ │ (real-time)│ │ │ │
29
+ │ └─────────────┘ │ ┌───────────────────┐ │ │
30
+ │ │ │ Sequel + SQLite │ │ │
31
+ │ ┌─────────────┐ │ └───────────────────┘ │ │
32
+ │ │ GTFS API │─────────▶│ │ │ │
33
+ │ │ (static) │ │ ▼ │ │
34
+ │ └─────────────┘ │ ~/.local/share/ │ │
35
+ │ │ njtransit/gtfs.sqlite3 │ │
36
+ │ └─────────────────────────┘ │
37
+ └─────────────────────────────────────────────────────────┘
38
+ ```
39
+
40
+ **Key decisions:**
41
+
42
+ - **Sequel gem** for database access (lightweight, clean DSL)
43
+ - **SQLite** stored in XDG-compliant location, configurable via `NJTransit.configure`
44
+ - **GTFS import is a deployment step** - errors raised if missing at runtime
45
+ - **Bus API enriches responses by default** with GTFS data
46
+
47
+ ## File Structure
48
+
49
+ ```
50
+ lib/njtransit/
51
+ ├── gtfs.rb # Main GTFS module (import, new, status)
52
+ ├── gtfs/
53
+ │ ├── database.rb # Sequel connection, schema setup
54
+ │ ├── importer.rb # Parses CSV files, populates DB
55
+ │ ├── models/
56
+ │ │ ├── agency.rb # Sequel::Model classes
57
+ │ │ ├── route.rb
58
+ │ │ ├── stop.rb
59
+ │ │ ├── trip.rb
60
+ │ │ ├── stop_time.rb
61
+ │ │ ├── calendar_date.rb
62
+ │ │ └── shape.rb
63
+ │ └── queries/
64
+ │ ├── routes_between.rb # Find routes serving two stops
65
+ │ └── schedule.rb # Full day schedule for route/stop
66
+ ├── tasks.rb # Rake task definitions
67
+ └── railtie.rb # Auto-load tasks in Rails
68
+
69
+ # Updated existing files:
70
+ lib/njtransit/configuration.rb # Add gtfs_database_path option
71
+ lib/njtransit/resources/bus.rb # Add enrichment logic
72
+ lib/njtransit/error.rb # Add GTFSNotImportedError
73
+ ```
74
+
75
+ ## Database Schema
76
+
77
+ ```sql
78
+ CREATE TABLE agencies (
79
+ agency_id TEXT PRIMARY KEY,
80
+ agency_name TEXT,
81
+ agency_url TEXT,
82
+ agency_timezone TEXT
83
+ );
84
+
85
+ CREATE TABLE routes (
86
+ route_id TEXT PRIMARY KEY,
87
+ agency_id TEXT,
88
+ route_short_name TEXT,
89
+ route_long_name TEXT,
90
+ route_type INTEGER,
91
+ route_color TEXT
92
+ );
93
+
94
+ CREATE TABLE stops (
95
+ stop_id TEXT PRIMARY KEY,
96
+ stop_code TEXT,
97
+ stop_name TEXT,
98
+ stop_lat REAL,
99
+ stop_lon REAL,
100
+ zone_id TEXT
101
+ );
102
+
103
+ CREATE TABLE trips (
104
+ trip_id TEXT PRIMARY KEY,
105
+ route_id TEXT,
106
+ service_id TEXT,
107
+ trip_headsign TEXT,
108
+ direction_id INTEGER,
109
+ shape_id TEXT
110
+ );
111
+
112
+ CREATE TABLE stop_times (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ trip_id TEXT,
115
+ stop_id TEXT,
116
+ arrival_time TEXT,
117
+ departure_time TEXT,
118
+ stop_sequence INTEGER
119
+ );
120
+
121
+ CREATE TABLE calendar_dates (
122
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
123
+ service_id TEXT,
124
+ date TEXT,
125
+ exception_type INTEGER
126
+ );
127
+
128
+ CREATE TABLE shapes (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ shape_id TEXT,
131
+ shape_pt_lat REAL,
132
+ shape_pt_lon REAL,
133
+ shape_pt_sequence INTEGER
134
+ );
135
+
136
+ -- Indexes
137
+ CREATE INDEX idx_stops_stop_code ON stops(stop_code);
138
+ CREATE INDEX idx_routes_short_name ON routes(route_short_name);
139
+ CREATE INDEX idx_trips_route_id ON trips(route_id);
140
+ CREATE INDEX idx_trips_service_id ON trips(service_id);
141
+ CREATE INDEX idx_stop_times_trip_id ON stop_times(trip_id);
142
+ CREATE INDEX idx_stop_times_stop_id ON stop_times(stop_id);
143
+ CREATE INDEX idx_calendar_dates_service_id_date ON calendar_dates(service_id, date);
144
+ CREATE INDEX idx_shapes_shape_id ON shapes(shape_id);
145
+ ```
146
+
147
+ ## GTFS Import API
148
+
149
+ ```ruby
150
+ # Import (deployment time)
151
+ NJTransit::GTFS.import("/path/to/bus_data/")
152
+ NJTransit::GTFS.import("/path/to/bus_data/", force: true) # Re-import, clears existing
153
+
154
+ # Status check
155
+ NJTransit::GTFS.status
156
+ # => {
157
+ # imported: true,
158
+ # path: "~/.local/share/njtransit/gtfs.sqlite3",
159
+ # routes: 261,
160
+ # stops: 16594,
161
+ # trips: 45843,
162
+ # imported_at: 2026-01-24
163
+ # }
164
+ ```
165
+
166
+ ### Rake Tasks
167
+
168
+ ```ruby
169
+ # In user's Rakefile
170
+ require 'njtransit/tasks'
171
+
172
+ # Provides:
173
+ # rake njtransit:gtfs:import[/path/to/bus_data]
174
+ # rake njtransit:gtfs:status
175
+ # rake njtransit:gtfs:clear
176
+ ```
177
+
178
+ Rails apps auto-load tasks via Railtie.
179
+
180
+ ## GTFS Query API
181
+
182
+ ```ruby
183
+ gtfs = NJTransit::GTFS.new
184
+
185
+ # Stops
186
+ gtfs.stops.all # => [Stop, Stop, ...]
187
+ gtfs.stops.find("stop_id_value") # => Stop (by stop_id)
188
+ gtfs.stops.find_by_code("21681") # => Stop (by stop_code)
189
+ gtfs.stops.where(zone_id: "NB") # => [Stop, ...]
190
+
191
+ # Routes
192
+ gtfs.routes.all
193
+ gtfs.routes.find("197") # => Route (by route_id or short_name)
194
+
195
+ # Route discovery
196
+ gtfs.routes_between(from: "WBRK", to: "PABT")
197
+ # => ["194", "197", "198"]
198
+
199
+ # Schedule
200
+ gtfs.schedule(route: "197", stop: "WBRK", date: Date.today)
201
+ # => [{ trip_id: "...", arrival_time: "06:45:00", departure_time: "06:45:00" }, ...]
202
+ ```
203
+
204
+ ### Model Objects
205
+
206
+ ```ruby
207
+ stop = gtfs.stops.find_by_code("21681")
208
+ stop.stop_id # => "1"
209
+ stop.stop_code # => "21681"
210
+ stop.stop_name # => "WILLOWBROOK MALL"
211
+ stop.lat # => 40.8523
212
+ stop.lon # => -74.2567
213
+ stop.to_h # => { stop_id: "1", stop_code: "21681", ... }
214
+ ```
215
+
216
+ ## Bus API Enrichment
217
+
218
+ Enrichment is **ON by default**. GTFS data is merged into Bus API responses automatically.
219
+
220
+ ```ruby
221
+ # Enriched (default)
222
+ client.bus.stops(route: "191", direction: "New York")
223
+ # => [{ "stop_id" => "21681", "stop_name" => "WILLOWBROOK MALL",
224
+ # "stop_lat" => 40.8523, "stop_lon" => -74.2567 }, ...]
225
+
226
+ # Opt-out
227
+ client.bus.stops(route: "191", direction: "New York", enrich: false)
228
+ # => Original unenriched response
229
+ ```
230
+
231
+ ### Enrichment by Method
232
+
233
+ | Method | Enrichment |
234
+ |--------|------------|
235
+ | `stops(route:, direction:)` | Add lat/lon from GTFS |
236
+ | `departures(stop:)` | Add stop lat/lon, route long_name |
237
+ | `stop_name(stop:)` | Add lat/lon |
238
+ | `stops_nearby(lat:, lon:)` | Already has coords, add zone_id |
239
+ | `vehicles_nearby(lat:, lon:)` | Add route long_name |
240
+
241
+ ### Error Handling
242
+
243
+ If GTFS hasn't been imported, enriched calls raise `GTFSNotImportedError`:
244
+
245
+ ```ruby
246
+ client.bus.stops(route: "191", direction: "New York")
247
+ # => raises NJTransit::GTFSNotImportedError:
248
+ # "GTFS data not found. Run: rake njtransit:gtfs:import[/path/to/bus_data]"
249
+ #
250
+ # Detected GTFS files at: ./docs/api/njtransit/bus_data/
251
+ # Hint: rake njtransit:gtfs:import[./docs/api/njtransit/bus_data/]
252
+ ```
253
+
254
+ Calls with `enrich: false` work without GTFS.
255
+
256
+ ## Configuration
257
+
258
+ ```ruby
259
+ NJTransit.configure do |config|
260
+ # Existing
261
+ config.username = "..."
262
+ config.password = "..."
263
+
264
+ # New
265
+ config.gtfs_database_path = "/custom/path/gtfs.sqlite3" # optional
266
+ end
267
+ ```
268
+
269
+ ### Default Path Resolution
270
+
271
+ 1. `config.gtfs_database_path` if set
272
+ 2. `$XDG_DATA_HOME/njtransit/gtfs.sqlite3` if XDG_DATA_HOME set
273
+ 3. `~/.local/share/njtransit/gtfs.sqlite3`
274
+
275
+ ### Auto-Detection Paths
276
+
277
+ For helpful error messages, these paths are checked for GTFS files:
278
+
279
+ ```ruby
280
+ GTFS_SEARCH_PATHS = [
281
+ "./bus_data",
282
+ "./vendor/bus_data",
283
+ "./docs/api/njtransit/bus_data",
284
+ "#{Gem.loaded_specs['njtransit']&.gem_dir}/data/bus_data"
285
+ ]
286
+ ```
287
+
288
+ ## Dependencies
289
+
290
+ ```ruby
291
+ # Gemfile additions
292
+ gem "sequel", "~> 5.0"
293
+ gem "sqlite3", "~> 2.0"
294
+ ```
295
+
296
+ ## Testing Strategy
297
+
298
+ ### Unit Tests (mocked, fast)
299
+
300
+ ```
301
+ spec/njtransit/gtfs/importer_spec.rb # CSV parsing, error handling
302
+ spec/njtransit/gtfs/models/*_spec.rb # Model attributes, queries
303
+ spec/njtransit/gtfs/queries/*_spec.rb # routes_between, schedule logic
304
+ ```
305
+
306
+ ### Integration Tests (real SQLite, fixtures)
307
+
308
+ ```
309
+ spec/integration/gtfs_spec.rb # Full import/query workflows
310
+ ```
311
+
312
+ ### Fixtures
313
+
314
+ ```
315
+ spec/fixtures/gtfs/
316
+ ├── agency.txt # 1 agency
317
+ ├── routes.txt # 10 routes
318
+ ├── stops.txt # 50 stops
319
+ ├── trips.txt # 100 trips
320
+ ├── stop_times.txt # 500 stop times
321
+ ├── calendar_dates.txt
322
+ └── shapes.txt
323
+ ```
324
+
325
+ ### Bus Enrichment Tests
326
+
327
+ ```
328
+ spec/njtransit/resources/bus_spec.rb
329
+ - Responses include GTFS fields when enriched
330
+ - enrich: false skips enrichment
331
+ - GTFSNotImportedError raised when DB missing
332
+ ```
333
+
334
+ ## Deployment
335
+
336
+ GTFS import runs once during deployment:
337
+
338
+ ```dockerfile
339
+ # Dockerfile
340
+ RUN bundle exec rake njtransit:gtfs:import[/app/vendor/bus_data/]
341
+ ```
342
+
343
+ ```ruby
344
+ # Capistrano
345
+ after 'deploy:published', 'njtransit:gtfs:import'
346
+ ```
347
+
348
+ ```yaml
349
+ # CI/CD pipeline
350
+ - run: bundle exec rake njtransit:gtfs:import[./bus_data/]
351
+ ```