debug-mcp 0.1.2

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +83 -0
  4. data/LICENSE +21 -0
  5. data/README.ja.md +383 -0
  6. data/README.md +384 -0
  7. data/examples/01_simple_bug.rb +43 -0
  8. data/examples/02_data_pipeline.rb +93 -0
  9. data/examples/03_recursion.rb +96 -0
  10. data/examples/RAILS_SCENARIOS.md +350 -0
  11. data/examples/SCENARIOS.md +142 -0
  12. data/examples/rails_test_app/setup.sh +428 -0
  13. data/examples/rails_test_app/testapp/.dockerignore +10 -0
  14. data/examples/rails_test_app/testapp/.ruby-version +1 -0
  15. data/examples/rails_test_app/testapp/Dockerfile +23 -0
  16. data/examples/rails_test_app/testapp/Gemfile +17 -0
  17. data/examples/rails_test_app/testapp/README.md +65 -0
  18. data/examples/rails_test_app/testapp/Rakefile +6 -0
  19. data/examples/rails_test_app/testapp/app/assets/images/.keep +0 -0
  20. data/examples/rails_test_app/testapp/app/assets/stylesheets/application.css +1 -0
  21. data/examples/rails_test_app/testapp/app/controllers/application_controller.rb +4 -0
  22. data/examples/rails_test_app/testapp/app/controllers/concerns/.keep +0 -0
  23. data/examples/rails_test_app/testapp/app/controllers/dashboard_controller.rb +38 -0
  24. data/examples/rails_test_app/testapp/app/controllers/health_controller.rb +11 -0
  25. data/examples/rails_test_app/testapp/app/controllers/orders_controller.rb +100 -0
  26. data/examples/rails_test_app/testapp/app/controllers/posts_controller.rb +82 -0
  27. data/examples/rails_test_app/testapp/app/controllers/sessions_controller.rb +25 -0
  28. data/examples/rails_test_app/testapp/app/controllers/users_controller.rb +44 -0
  29. data/examples/rails_test_app/testapp/app/helpers/application_helper.rb +2 -0
  30. data/examples/rails_test_app/testapp/app/models/application_record.rb +3 -0
  31. data/examples/rails_test_app/testapp/app/models/comment.rb +8 -0
  32. data/examples/rails_test_app/testapp/app/models/concerns/.keep +0 -0
  33. data/examples/rails_test_app/testapp/app/models/order.rb +56 -0
  34. data/examples/rails_test_app/testapp/app/models/order_item.rb +16 -0
  35. data/examples/rails_test_app/testapp/app/models/post.rb +29 -0
  36. data/examples/rails_test_app/testapp/app/models/user.rb +34 -0
  37. data/examples/rails_test_app/testapp/app/services/order_report_service.rb +40 -0
  38. data/examples/rails_test_app/testapp/app/views/layouts/application.html.erb +28 -0
  39. data/examples/rails_test_app/testapp/app/views/pwa/manifest.json.erb +22 -0
  40. data/examples/rails_test_app/testapp/app/views/pwa/service-worker.js +26 -0
  41. data/examples/rails_test_app/testapp/bin/ci +6 -0
  42. data/examples/rails_test_app/testapp/bin/dev +2 -0
  43. data/examples/rails_test_app/testapp/bin/rails +4 -0
  44. data/examples/rails_test_app/testapp/bin/rake +4 -0
  45. data/examples/rails_test_app/testapp/bin/setup +35 -0
  46. data/examples/rails_test_app/testapp/config/application.rb +42 -0
  47. data/examples/rails_test_app/testapp/config/boot.rb +3 -0
  48. data/examples/rails_test_app/testapp/config/ci.rb +14 -0
  49. data/examples/rails_test_app/testapp/config/database.yml +32 -0
  50. data/examples/rails_test_app/testapp/config/environment.rb +5 -0
  51. data/examples/rails_test_app/testapp/config/environments/development.rb +54 -0
  52. data/examples/rails_test_app/testapp/config/environments/production.rb +67 -0
  53. data/examples/rails_test_app/testapp/config/environments/test.rb +42 -0
  54. data/examples/rails_test_app/testapp/config/initializers/content_security_policy.rb +29 -0
  55. data/examples/rails_test_app/testapp/config/initializers/filter_parameter_logging.rb +8 -0
  56. data/examples/rails_test_app/testapp/config/initializers/inflections.rb +16 -0
  57. data/examples/rails_test_app/testapp/config/locales/en.yml +31 -0
  58. data/examples/rails_test_app/testapp/config/puma.rb +39 -0
  59. data/examples/rails_test_app/testapp/config/routes.rb +34 -0
  60. data/examples/rails_test_app/testapp/config.ru +6 -0
  61. data/examples/rails_test_app/testapp/db/migrate/20260216002916_create_users.rb +12 -0
  62. data/examples/rails_test_app/testapp/db/migrate/20260216002919_create_posts.rb +13 -0
  63. data/examples/rails_test_app/testapp/db/migrate/20260216002922_create_comments.rb +11 -0
  64. data/examples/rails_test_app/testapp/db/migrate/20260222000001_create_orders.rb +14 -0
  65. data/examples/rails_test_app/testapp/db/migrate/20260222000002_create_order_items.rb +13 -0
  66. data/examples/rails_test_app/testapp/db/schema.rb +71 -0
  67. data/examples/rails_test_app/testapp/db/seeds.rb +85 -0
  68. data/examples/rails_test_app/testapp/docker-compose.yml +21 -0
  69. data/examples/rails_test_app/testapp/docker-entrypoint.sh +10 -0
  70. data/examples/rails_test_app/testapp/lib/tasks/.keep +0 -0
  71. data/examples/rails_test_app/testapp/log/.keep +0 -0
  72. data/examples/rails_test_app/testapp/public/400.html +135 -0
  73. data/examples/rails_test_app/testapp/public/404.html +135 -0
  74. data/examples/rails_test_app/testapp/public/406-unsupported-browser.html +135 -0
  75. data/examples/rails_test_app/testapp/public/422.html +135 -0
  76. data/examples/rails_test_app/testapp/public/500.html +135 -0
  77. data/examples/rails_test_app/testapp/public/icon.png +0 -0
  78. data/examples/rails_test_app/testapp/public/icon.svg +3 -0
  79. data/examples/rails_test_app/testapp/public/robots.txt +1 -0
  80. data/examples/rails_test_app/testapp/script/.keep +0 -0
  81. data/examples/rails_test_app/testapp/storage/.keep +0 -0
  82. data/examples/rails_test_app/testapp/tmp/.keep +0 -0
  83. data/examples/rails_test_app/testapp/tmp/pids/.keep +0 -0
  84. data/examples/rails_test_app/testapp/tmp/storage/.keep +0 -0
  85. data/examples/rails_test_app/testapp/vendor/.keep +0 -0
  86. data/exe/debug-mcp +39 -0
  87. data/exe/debug-rails +127 -0
  88. data/lib/debug_mcp/client_cleanup.rb +102 -0
  89. data/lib/debug_mcp/code_safety_analyzer.rb +124 -0
  90. data/lib/debug_mcp/debug_client.rb +1143 -0
  91. data/lib/debug_mcp/exit_message_builder.rb +112 -0
  92. data/lib/debug_mcp/pending_http_helper.rb +25 -0
  93. data/lib/debug_mcp/rails_helper.rb +155 -0
  94. data/lib/debug_mcp/server.rb +364 -0
  95. data/lib/debug_mcp/session_manager.rb +436 -0
  96. data/lib/debug_mcp/stop_event_annotator.rb +152 -0
  97. data/lib/debug_mcp/tcp_session_discovery.rb +226 -0
  98. data/lib/debug_mcp/tools/connect.rb +669 -0
  99. data/lib/debug_mcp/tools/continue_execution.rb +161 -0
  100. data/lib/debug_mcp/tools/disconnect.rb +169 -0
  101. data/lib/debug_mcp/tools/evaluate_code.rb +354 -0
  102. data/lib/debug_mcp/tools/finish.rb +84 -0
  103. data/lib/debug_mcp/tools/get_context.rb +217 -0
  104. data/lib/debug_mcp/tools/get_source.rb +193 -0
  105. data/lib/debug_mcp/tools/inspect_object.rb +107 -0
  106. data/lib/debug_mcp/tools/list_debug_sessions.rb +60 -0
  107. data/lib/debug_mcp/tools/list_files.rb +189 -0
  108. data/lib/debug_mcp/tools/list_paused_sessions.rb +108 -0
  109. data/lib/debug_mcp/tools/next.rb +70 -0
  110. data/lib/debug_mcp/tools/rails_info.rb +200 -0
  111. data/lib/debug_mcp/tools/rails_model.rb +362 -0
  112. data/lib/debug_mcp/tools/rails_routes.rb +186 -0
  113. data/lib/debug_mcp/tools/read_file.rb +214 -0
  114. data/lib/debug_mcp/tools/remove_breakpoint.rb +173 -0
  115. data/lib/debug_mcp/tools/run_debug_command.rb +55 -0
  116. data/lib/debug_mcp/tools/run_script.rb +293 -0
  117. data/lib/debug_mcp/tools/set_breakpoint.rb +206 -0
  118. data/lib/debug_mcp/tools/step.rb +67 -0
  119. data/lib/debug_mcp/tools/trigger_request.rb +515 -0
  120. data/lib/debug_mcp/version.rb +5 -0
  121. data/lib/debug_mcp.rb +40 -0
  122. metadata +251 -0
data/README.md ADDED
@@ -0,0 +1,384 @@
1
+ # debug-mcp
2
+
3
+ [日本語版 (Japanese)](README.ja.md)
4
+
5
+ MCP (Model Context Protocol) server that connects LLM agents to Ruby's [debug gem](https://github.com/ruby/debug), giving them access to the runtime context of paused Ruby processes.
6
+
7
+ LLM agents can connect to a paused Ruby process, inspect variables, evaluate code, set breakpoints, and control execution — all through MCP tool calls.
8
+
9
+ ## What it does
10
+
11
+ Existing Ruby/Rails MCP servers only provide static analysis or application-level APIs. debug-mcp goes further: it connects to **running Ruby processes** via the debug gem and exposes their runtime state to LLM agents.
12
+
13
+ ```
14
+ Agent → connect(host: "localhost", port: 12345)
15
+ Agent → get_context()
16
+ → local variables, instance variables, call stack
17
+ Agent → evaluate_code(code: "user.valid?")
18
+ → false
19
+ Agent → evaluate_code(code: "user.errors.full_messages")
20
+ → ["Email can't be blank"]
21
+ Agent → continue_execution()
22
+ ```
23
+
24
+ ## Installation
25
+
26
+ ```ruby
27
+ gem "debug-mcp"
28
+ ```
29
+
30
+ Or install directly:
31
+
32
+ ```
33
+ gem install debug-mcp
34
+ ```
35
+
36
+ Requires Ruby >= 3.2.0.
37
+
38
+ > **Migrating from `girb-mcp`?** This gem was previously published as `girb-mcp`
39
+ > on RubyGems (last version: 0.1.1). It was renamed to `debug-mcp` starting from
40
+ > 0.1.2 to better reflect its purpose. Replace `gem "girb-mcp"` with
41
+ > `gem "debug-mcp"` and update your MCP client config (`girb-mcp` → `debug-mcp`,
42
+ > `girb-rails` → `debug-rails`). See [CHANGELOG.md](CHANGELOG.md) for details.
43
+
44
+ ## Quick Start
45
+
46
+ ### 1. Start a Ruby process with the debugger
47
+
48
+ ```bash
49
+ # Script
50
+ rdbg --open --port=12345 my_script.rb
51
+
52
+ # Or with environment variables
53
+ RUBY_DEBUG_OPEN=true RUBY_DEBUG_PORT=12345 ruby my_script.rb
54
+
55
+ # Or add `debugger` / `binding.break` in your code and run with rdbg
56
+ rdbg --open my_script.rb
57
+ ```
58
+
59
+ ### 2. Configure your MCP client
60
+
61
+ debug-mcp works with any MCP-compatible client. Add it to your client's MCP server configuration:
62
+
63
+ #### Claude Code
64
+
65
+ Add to `~/.claude/settings.json` (or project `.claude/settings.json`):
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "debug-mcp": {
71
+ "command": "debug-mcp",
72
+ "args": []
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ If using Bundler:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "debug-mcp": {
84
+ "command": "bundle",
85
+ "args": ["exec", "debug-mcp"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ #### Gemini CLI
92
+
93
+ Add to `~/.gemini/settings.json`:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "debug-mcp": {
99
+ "command": "debug-mcp",
100
+ "args": []
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ If using Bundler:
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "debug-mcp": {
112
+ "command": "bundle",
113
+ "args": ["exec", "debug-mcp"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### 3. Start debugging
120
+
121
+ Ask your agent to connect and debug:
122
+
123
+ > "Connect to the debug session on port 12345 and show me the current state"
124
+
125
+ > "Set a breakpoint at app/models/user.rb line 42 and send a GET request to /users/1"
126
+
127
+ ## Usage
128
+
129
+ ```
130
+ Usage: debug-mcp [options]
131
+ -t, --transport TRANSPORT Transport type: stdio (default) or http
132
+ -p, --port PORT HTTP port (default: 6029, only for http transport)
133
+ --host HOST HTTP host (default: 127.0.0.1, only for http transport)
134
+ --session-timeout SECONDS Session timeout in seconds (default: 1800)
135
+ -v, --version Show version
136
+ -h, --help Show this help
137
+ ```
138
+
139
+ ### STDIO transport (default)
140
+
141
+ Standard transport for MCP clients. No additional configuration needed.
142
+
143
+ ```bash
144
+ debug-mcp
145
+ ```
146
+
147
+ ### HTTP transport (Streamable HTTP)
148
+
149
+ For browser-based clients or other HTTP-compatible MCP clients.
150
+
151
+ ```bash
152
+ debug-mcp --transport http --port 8080
153
+ ```
154
+
155
+ The MCP endpoint will be available at `http://127.0.0.1:8080/mcp`.
156
+
157
+ ### Session timeout
158
+
159
+ Debug sessions are automatically cleaned up after 30 minutes of inactivity. Adjust with:
160
+
161
+ ```bash
162
+ debug-mcp --session-timeout 3600 # 1 hour
163
+ ```
164
+
165
+ The session manager also detects and cleans up sessions whose target process has exited.
166
+
167
+ ## Tools
168
+
169
+ ### Discovery & Connection
170
+
171
+ | Tool | Description |
172
+ |------|-------------|
173
+ | `list_debug_sessions` | List available debug sessions (Unix sockets) |
174
+ | `connect` | Connect to a debug session via socket path or TCP |
175
+ | `list_paused_sessions` | List currently connected sessions |
176
+
177
+ ### Investigation
178
+
179
+ | Tool | Description |
180
+ |------|-------------|
181
+ | `evaluate_code` | Execute Ruby code in the stopped binding |
182
+ | `inspect_object` | Get class, value, and instance variables of an object |
183
+ | `get_context` | Local variables, instance variables, call stack, breakpoints |
184
+ | `get_source` | Source code of a method or class |
185
+ | `read_file` | Read source files with optional line range |
186
+ | `list_files` | List files in a directory, with optional glob pattern |
187
+
188
+ ### Execution Control
189
+
190
+ | Tool | Description |
191
+ |------|-------------|
192
+ | `set_breakpoint` | Set a breakpoint: line (file + line), method (`User#save`), or exception class |
193
+ | `remove_breakpoint` | Remove a breakpoint by file + line, method name, exception class, or number |
194
+ | `continue_execution` | Resume execution until next breakpoint or exit |
195
+ | `step` | Step into the next method call |
196
+ | `next` | Step over to the next line |
197
+ | `finish` | Run until the current method/block returns |
198
+ | `run_debug_command` | Execute any raw debugger command |
199
+ | `disconnect` | Disconnect from the session and terminate the process |
200
+
201
+ ### Entry Points
202
+
203
+ | Tool | Description |
204
+ |------|-------------|
205
+ | `run_script` | Start a Ruby script under rdbg and connect to it |
206
+ | `trigger_request` | Send an HTTP request to a Rails app under debug |
207
+
208
+ ### Rails Tools (auto-detected)
209
+
210
+ These tools are automatically registered when a Rails process is detected.
211
+
212
+ | Tool | Description |
213
+ |------|-------------|
214
+ | `rails_info` | Show app name, Rails/Ruby versions, environment, root path |
215
+ | `rails_routes` | Show routes (verb, path, controller#action), filterable by controller or path |
216
+ | `rails_model` | Show model structure: columns, associations, validations, enums, scopes |
217
+
218
+ ## Workflows
219
+
220
+ ### Debug a Ruby script
221
+
222
+ ```
223
+ Agent: run_script(file: "my_script.rb")
224
+ Agent: get_context()
225
+ Agent: evaluate_code(code: "result")
226
+ Agent: next()
227
+ Agent: evaluate_code(code: "result")
228
+ Agent: continue_execution()
229
+ ```
230
+
231
+ ### Method breakpoints
232
+
233
+ ```
234
+ Agent: run_script(file: "my_script.rb", breakpoints: ["DataPipeline#validate"])
235
+ → Script starts and pauses at DataPipeline#validate
236
+ Agent: evaluate_code(code: "records")
237
+ Agent: continue_execution()
238
+ ```
239
+
240
+ ### Catch and debug exceptions
241
+
242
+ ```
243
+ Agent: run_script(file: "my_script.rb")
244
+ Agent: set_breakpoint(exception_class: "NoMethodError")
245
+ Agent: continue_execution()
246
+ → Execution pauses BEFORE the exception propagates
247
+ Agent: get_context()
248
+ Agent: evaluate_code(code: "$!.message")
249
+ ```
250
+
251
+ ### Restart after a crash
252
+
253
+ ```
254
+ → Program crashed with NoMethodError
255
+ Agent: run_script(file: "my_script.rb", restore_breakpoints: true)
256
+ → Same breakpoints restored automatically
257
+ Agent: set_breakpoint(exception_class: "NoMethodError")
258
+ Agent: continue_execution()
259
+ → Catches the exception before it crashes
260
+ ```
261
+
262
+ ### Debug a Rails request
263
+
264
+ Start your Rails server with debug enabled using `debug-rails`:
265
+
266
+ ```bash
267
+ debug-rails # equivalent to RUBY_DEBUG_OPEN=true bin/rails server
268
+ debug-rails s -p 4000 # specify port
269
+ debug-rails --debug-port 3333 # use specific TCP debug port (useful in Docker)
270
+ ```
271
+
272
+ Then ask the agent to debug:
273
+
274
+ ```
275
+ Agent: connect()
276
+ Agent: set_breakpoint(file: "app/controllers/users_controller.rb", line: 15)
277
+ Agent: trigger_request(method: "GET", url: "http://localhost:3000/users/1")
278
+ Agent: get_context()
279
+ Agent: evaluate_code(code: "@user.attributes")
280
+ Agent: continue_execution()
281
+ ```
282
+
283
+ ### Debug a Dockerized Rails app
284
+
285
+ > **Security note:** The debug gem has no authentication. Anyone who can reach the debug port can execute arbitrary code inside the container. Always restrict access as shown below.
286
+
287
+ #### Option A: Localhost-only TCP (simple)
288
+
289
+ Expose the debug port bound to `127.0.0.1` so only local processes can connect:
290
+
291
+ ```yaml
292
+ services:
293
+ web:
294
+ build: .
295
+ ports:
296
+ - "3000:3000"
297
+ - "127.0.0.1:12345:12345" # localhost only
298
+ environment:
299
+ - RUBY_DEBUG_OPEN=true
300
+ - RUBY_DEBUG_HOST=0.0.0.0
301
+ - RUBY_DEBUG_PORT=12345
302
+ ```
303
+
304
+ ```
305
+ Agent: connect(port: 12345)
306
+ ```
307
+
308
+ The agent can read source files inside the container — no local copy of the source code is needed.
309
+
310
+ #### Option B: Unix socket volume mount (recommended)
311
+
312
+ Mount a shared directory for the debug socket. No port exposure needed:
313
+
314
+ ```yaml
315
+ services:
316
+ web:
317
+ build: .
318
+ ports:
319
+ - "3000:3000"
320
+ environment:
321
+ - RUBY_DEBUG_OPEN=true
322
+ - RUBY_DEBUG_SOCK_PATH=/debug/rdbg.sock
323
+ volumes:
324
+ - debug_sock:/debug
325
+
326
+ volumes:
327
+ debug_sock:
328
+ ```
329
+
330
+ ```
331
+ Agent: connect(path: "/path/to/debug_sock/rdbg.sock")
332
+ ```
333
+
334
+ ### Connect to an existing breakpoint
335
+
336
+ ```bash
337
+ # Terminal: your app hits a `debugger` statement
338
+ rdbg --open my_app.rb
339
+ ```
340
+
341
+ ```
342
+ Agent: list_debug_sessions()
343
+ Agent: connect(path: "/tmp/rdbg-1000/rdbg-12345")
344
+ Agent: get_context()
345
+ ```
346
+
347
+ ## How it works
348
+
349
+ ```
350
+ ┌────────────┐ STDIO or Streamable HTTP ┌───────────┐ TCP/Unix Socket ┌──────────────┐
351
+ │ MCP Client │ ◄────────────────────────► │ debug-mcp │ ◄──────────────────► │ Ruby process │
352
+ │ │ (JSON-RPC) │(MCP Server)│ debug gem proto │ (rdbg) │
353
+ └────────────┘ └───────────┘ └──────────────┘
354
+ ```
355
+
356
+ 1. debug-mcp runs as an MCP server communicating via STDIO (default) or Streamable HTTP
357
+ 2. The debug gem (`rdbg --open`) exposes a socket on the target Ruby process
358
+ 3. debug-mcp connects to that socket using the debug gem's wire protocol
359
+ 4. MCP tool calls are translated to debugger commands and results are returned
360
+ 5. Idle sessions are automatically cleaned up after a configurable timeout
361
+
362
+ ## Security
363
+
364
+ debug-mcp is a debugging tool that intentionally provides deep runtime access. Here's what you should know:
365
+
366
+ **Structured tools minimize arbitrary code execution.** Most debugging tasks — viewing variables, reading source code, inspecting model structure — are handled by dedicated tools that don't run arbitrary code. `evaluate_code` is available for runtime inspection, and a built-in safety checker warns about dangerous operations.
367
+
368
+ **The debug gem has no authentication.** Anyone who can reach the debug socket can execute arbitrary code in the target process. Always bind to localhost (`127.0.0.1`) or use Unix sockets. See the [Docker section](#debug-a-dockerized-rails-app) for configuration examples.
369
+
370
+ ## Related projects
371
+
372
+ - [girb](https://github.com/rira100000000/girb) — AI-powered IRB assistant for humans, sharing the same philosophy.
373
+
374
+ ## Development
375
+
376
+ ```bash
377
+ git clone https://github.com/rira100000000/debug-mcp.git
378
+ cd debug-mcp
379
+ bundle install
380
+ ```
381
+
382
+ ## License
383
+
384
+ MIT
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # シナリオ: 割引計算にバグがある
4
+ # 期待: 合計金額が1000円以上なら10%割引
5
+ # 実際: 割引が正しく適用されない
6
+
7
+ class Cart
8
+ attr_reader :items
9
+
10
+ def initialize
11
+ @items = []
12
+ end
13
+
14
+ def add(name, price, quantity = 1)
15
+ @items << { name: name, price: price, quantity: quantity }
16
+ end
17
+
18
+ def subtotal
19
+ @items.sum { |item| item[:price] * item[:quantity] }
20
+ end
21
+
22
+ def discount_rate
23
+ subtotal > 1000 ? 0.1 : 0
24
+ end
25
+
26
+ def total
27
+ sub = subtotal
28
+ discount = sub * discount_rate
29
+ sub + discount # BUG: should be sub - discount
30
+ end
31
+ end
32
+
33
+ cart = Cart.new
34
+ cart.add("りんご", 200, 3)
35
+ cart.add("バナナ", 150, 2)
36
+ cart.add("みかん", 100, 4)
37
+
38
+ debugger
39
+
40
+ puts "小計: #{cart.subtotal}円"
41
+ puts "割引率: #{cart.discount_rate * 100}%"
42
+ puts "合計: #{cart.total}円"
43
+ puts "期待される合計: #{cart.subtotal * (1 - cart.discount_rate)}円"
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # シナリオ: CSVデータ変換パイプラインで一部のレコードが消える
4
+ # ステップ実行で各段階のデータを追跡する
5
+
6
+ require "csv"
7
+ require "stringio"
8
+
9
+ csv_data = <<~CSV
10
+ id,name,age,score
11
+ 1,田中太郎,28,85
12
+ 2,鈴木花子,,92
13
+ 3,佐藤次郎,35,
14
+ 4,山田三郎,42,78
15
+ 5,高橋四郎,-3,95
16
+ CSV
17
+
18
+ class DataPipeline
19
+ def initialize(csv_string)
20
+ @raw = CSV.parse(csv_string, headers: true)
21
+ @records = []
22
+ @errors = []
23
+ end
24
+
25
+ def run
26
+ parse
27
+ validate
28
+ transform
29
+ { records: @records, errors: @errors, stats: stats }
30
+ end
31
+
32
+ private
33
+
34
+ def parse
35
+ @records = @raw.map do |row|
36
+ {
37
+ id: row["id"]&.to_i,
38
+ name: row["name"],
39
+ age: row["age"]&.to_i, # nil.to_i => 0
40
+ score: row["score"]&.to_i, # nil.to_i => 0
41
+ }
42
+ end
43
+ end
44
+
45
+ def validate
46
+ @records.reject! do |r|
47
+ if r[:age] <= 0
48
+ @errors << { id: r[:id], reason: "invalid age: #{r[:age]}" }
49
+ true
50
+ elsif r[:score] <= 0
51
+ @errors << { id: r[:id], reason: "invalid score: #{r[:score]}" }
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+ end
58
+
59
+ def transform
60
+ avg = @records.sum { |r| r[:score] } / @records.size.to_f
61
+ @records.each do |r|
62
+ r[:grade] = case r[:score]
63
+ when 90.. then "A"
64
+ when 80.. then "B"
65
+ when 70.. then "C"
66
+ else "D"
67
+ end
68
+ r[:above_average] = r[:score] > avg
69
+ end
70
+ end
71
+
72
+ def stats
73
+ {
74
+ total_input: @raw.size,
75
+ valid_records: @records.size,
76
+ error_count: @errors.size,
77
+ average_score: @records.sum { |r| r[:score] } / @records.size.to_f,
78
+ }
79
+ end
80
+ end
81
+
82
+ pipeline = DataPipeline.new(csv_data)
83
+
84
+ debugger
85
+
86
+ result = pipeline.run
87
+
88
+ puts "=== 結果 ==="
89
+ puts "有効レコード: #{result[:records].size}"
90
+ result[:records].each { |r| puts " #{r}" }
91
+ puts "エラー: #{result[:errors].size}"
92
+ result[:errors].each { |e| puts " #{e}" }
93
+ puts "統計: #{result[:stats]}"
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # シナリオ: 木構造の探索で特定ノードの状態を調査
4
+ # ブレークポイントを条件付きで設定して、特定の条件で止める
5
+
6
+ class TreeNode
7
+ attr_accessor :value, :children, :metadata
8
+
9
+ def initialize(value, metadata: {})
10
+ @value = value
11
+ @children = []
12
+ @metadata = metadata
13
+ end
14
+
15
+ def add(*values)
16
+ values.each do |v|
17
+ child = TreeNode.new(v)
18
+ @children << child
19
+ yield child if block_given?
20
+ end
21
+ self
22
+ end
23
+
24
+ def find(target)
25
+ return self if value == target
26
+
27
+ children.each do |child|
28
+ result = child.find(target)
29
+ return result if result
30
+ end
31
+
32
+ nil
33
+ end
34
+
35
+ def depth_first(&block)
36
+ block.call(self)
37
+ children.each { |c| c.depth_first(&block) }
38
+ end
39
+
40
+ def total_nodes
41
+ 1 + children.sum(&:total_nodes)
42
+ end
43
+
44
+ def max_depth(current = 0)
45
+ if children.empty?
46
+ current
47
+ else
48
+ children.map { |c| c.max_depth(current + 1) }.max
49
+ end
50
+ end
51
+
52
+ def to_s(indent = 0)
53
+ result = "#{" " * indent}#{value}\n"
54
+ children.each { |c| result += c.to_s(indent + 2) }
55
+ result
56
+ end
57
+ end
58
+
59
+ # 組織図を構築
60
+ company = TreeNode.new("CEO")
61
+
62
+ company.add("CTO") do |cto|
63
+ cto.add("VP Engineering") do |vp|
64
+ vp.add("Team Lead A") do |tl|
65
+ tl.add("Engineer 1", "Engineer 2", "Engineer 3")
66
+ end
67
+ vp.add("Team Lead B") do |tl|
68
+ tl.add("Engineer 4", "Engineer 5")
69
+ end
70
+ end
71
+ cto.add("VP Product") do |vp|
72
+ vp.add("PM 1", "PM 2")
73
+ end
74
+ end
75
+
76
+ company.add("CFO") do |cfo|
77
+ cfo.add("Controller") do |ctrl|
78
+ ctrl.add("Accountant 1", "Accountant 2")
79
+ end
80
+ end
81
+
82
+ company.add("COO") do |coo|
83
+ coo.add("VP Operations") do |vp|
84
+ vp.add("Manager 1", "Manager 2", "Manager 3")
85
+ end
86
+ end
87
+
88
+ debugger
89
+
90
+ puts company.to_s
91
+ puts "Total nodes: #{company.total_nodes}"
92
+ puts "Max depth: #{company.max_depth}"
93
+
94
+ # 特定のノードを検索
95
+ target = company.find("Engineer 3")
96
+ puts "Found: #{target&.value || 'not found'}"