Module: RailsAiContext::Serializers::ToolGuideHelper
- Includes:
- CountPhrase
- Included in:
- ClaudeRulesSerializer, ClaudeSerializer, CopilotInstructionsSerializer, CopilotSerializer, CursorRulesSerializer, OpencodeSerializer
- Defined in:
- lib/rails_ai_context/serializers/tool_guide_helper.rb
Overview
Shared helper for rendering the tool reference section in context files. Reads config.tool_mode to generate MCP syntax, CLI syntax, or both.
Instance Method Summary collapse
-
#api_endpoint_workflow_lines ⇒ Object
API-only apps have no view layer - swap in a workflow for modifying a JSON/XML response instead.
-
#api_only? ⇒ Boolean
True when the app runs in API-only mode (no view layer) - used to swap the view-editing workflow for an API-focused one in the generated guide.
- #build_tools_table(include_mcp:) ⇒ Object
-
#render_tools_guide ⇒ Object
Full tool guide section - used by split rules files (.claude/rules/, .cursor/rules/, etc.).
-
#render_tools_guide_compact ⇒ Object
Compact tool guide for root files (CLAUDE.md, AGENTS.md) that have line limits.
-
#tool_call(mcp_call, cli_call) ⇒ Object
Returns the tool invocation example for a given tool call.
-
#tool_count ⇒ Object
Derived from BaseTool.registered_tools - the single source of truth for tool count.
- #tool_mode ⇒ Object
-
#tool_rows ⇒ Object
Every registered tool declares its own row, so the guide lists what the registry holds and nothing else.
- #tools_anti_hallucination_section ⇒ Object
- #tools_antipatterns_section ⇒ Object
- #tools_detail_guidance ⇒ Object
- #tools_header ⇒ Object
- #tools_intro ⇒ Object
-
#tools_name_list ⇒ Object
Dense one-line-per-tool listing, from the same registry the table uses.
- #tools_power_tool_section ⇒ Object
- #tools_rules_section ⇒ Object
- #tools_table ⇒ Object
- #tools_workflow_section ⇒ Object
-
#view_workflow_lines ⇒ Object
HTML/Hotwire apps get the view-editing workflow.
Methods included from CountPhrase
Instance Method Details
#api_endpoint_workflow_lines ⇒ Object
API-only apps have no view layer - swap in a workflow for modifying a JSON/XML response instead.
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 178 def api_endpoint_workflow_lines [ "**Modify a JSON endpoint** (add/change a serialized field, adjust status codes):", "1. #{tool_call_inline("rails_get_controllers", "controller:\"PostsController\", action:\"create\"", "controllers", "controller=PostsController action=create")} - action source + strong params + render map", "2. #{tool_call_inline("rails_get_model_details", "model:\"Post\"", "model_details", "model=Post")} - schema + associations + validations backing the response", "3. Read the controller file, make your edit", "4. #{tool_call_inline("rails_validate", "files:[\"app/controllers/posts_controller.rb\"], level:\"rails\"", "validate", "files=app/controllers/posts_controller.rb level=rails")}", "" ] end |
#api_only? ⇒ Boolean
True when the app runs in API-only mode (no view layer) - used to swap
the view-editing workflow for an API-focused one in the generated guide.
Falls back to false (the view workflow) when the includer has no
context to inspect.
32 33 34 35 36 37 38 39 40 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 32 def api_only? return false unless respond_to?(:context) ctx = context return true if ctx.is_a?(Hash) && ctx.dig(:api, :api_only) == true arch = ctx.is_a?(Hash) ? ctx.dig(:conventions, :architecture) : nil arch.is_a?(Array) && arch.include?("api_only") end |
#build_tools_table(include_mcp:) ⇒ Object
250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 250 def build_tools_table(include_mcp:) # For CLI-only tables, `match_type=any` uses `=` (not `:`), so we tweak description. rows = tool_rows.map do |tool, row| cli = cli_cmd(RailsAiContext::CLI::ToolRunner.short_name(tool.tool_name), row.cli_args) if include_mcp "| `#{row.mcp}` | `#{cli}` | #{row.summary} |" else "| `#{cli}` | #{row.summary.gsub('match_type:"any"', "match_type=any")} |" end end header = include_mcp ? [ "| MCP | CLI | What it does |", "|-----|-----|-------------|" ] : [ "| CLI | What it does |", "|-----|-------------|" ] header + rows end |
#render_tools_guide ⇒ Object
Full tool guide section - used by split rules files (.claude/rules/, .cursor/rules/, etc.)
265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 265 def render_tools_guide lines = [] lines << tools_header lines << "" lines.concat(tools_intro) lines.concat(tools_anti_hallucination_section) lines.concat(tools_detail_guidance) lines.concat(tools_power_tool_section) lines.concat(tools_workflow_section) lines.concat(tools_antipatterns_section) lines.concat(tools_rules_section) lines.concat(tools_table) lines end |
#render_tools_guide_compact ⇒ Object
Compact tool guide for root files (CLAUDE.md, AGENTS.md) that have line limits. Includes power tools + workflows + rules + dense tool name list (no table).
282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 282 def render_tools_guide_compact lines = [] lines << tools_header lines << "" lines.concat(tools_intro) lines.concat(tools_anti_hallucination_section) lines.concat(tools_power_tool_section) lines.concat(tools_workflow_section) lines.concat(tools_antipatterns_section) lines.concat(tools_rules_section) lines.concat(tools_name_list) lines end |
#tool_call(mcp_call, cli_call) ⇒ Object
Returns the tool invocation example for a given tool call. MCP: rails_analyze_feature(feature:"post") CLI: rails 'ai:tool' feature=post
13 14 15 16 17 18 19 20 21 22 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 13 def tool_call(mcp_call, cli_call) case tool_mode when :cli "→ `#{cli_call}`" when :mcp "→ MCP: `#{mcp_call}`\n→ CLI: `#{cli_call}`" else "→ `#{mcp_call}`" end end |
#tool_count ⇒ Object
Derived from BaseTool.registered_tools - the single source of truth for tool count.
43 44 45 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 43 def tool_count RailsAiContext::Server.builtin_tools.size end |
#tool_mode ⇒ Object
24 25 26 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 24 def tool_mode RailsAiContext.configuration.tool_mode end |
#tool_rows ⇒ Object
Every registered tool declares its own row, so the guide lists what the registry holds and nothing else.
244 245 246 247 248 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 244 def tool_rows RailsAiContext::Tools::BaseTool.registered_tools .filter_map { |tool| [ tool, tool.guide_row ] if tool.guide_row } .sort_by { |_tool, row| row.order } end |
#tools_anti_hallucination_section ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 72 def tools_anti_hallucination_section return [] unless RailsAiContext.configuration.anti_hallucination_rules [ "### Anti-Hallucination Protocol - Verify Before You Write", "", "AI assistants produce confident-wrong code when statistical priors from training", "data override observed facts in the current project. These 6 rules force", "verification at the exact moments hallucination is most likely.", "", "1. **Verify before you write.** Never reference a column, association, route, helper, method, class, partial, or gem you have NOT verified in THIS project via a tool call in THIS turn. If it's not verified here, verify it now. Never invent names that \"sound right.\"", "2. **Mark every assumption.** If you must proceed without verification, prefix the relevant output with `[ASSUMPTION]` and state what you're assuming and why. Silent assumptions are forbidden. \"I'd need to check X first\" is a valid and preferred answer.", "3. **Training data describes average Rails. This app isn't average.** When something feels \"obviously\" like standard Rails, query anyway. Factories vs fixtures? Pundit vs CanCan? Devise vs has_secure_password? Check `rails_get_conventions` and `rails_get_gems` BEFORE scaffolding anything.", "4. **Check the inheritance chain before every edit.** Before writing a controller action: inherited `before_action` filters and ancestor classes. Before writing a model method: concerns, includes, STI parents. Inheritance is never flat.", "5. **Empty tool output is information, not permission.** \"0 callers found,\" \"no validations,\" or a missing model is a signal to investigate or confirm with the user - not a license to proceed on guesses. Follow `_Next:` hints.", "6. **Stale context lies. Re-query after writes.** After any edit, tool output from earlier in this turn may be wrong. Re-query the affected tool before the next write.", "" ] end |
#tools_antipatterns_section ⇒ Object
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 189 def tools_antipatterns_section search_tool = tool_mode == :cli ? cli_cmd("search_code") : "rails_search_code" validate_tool = tool_mode == :cli ? cli_cmd("validate") : "rails_validate" [ "### Common mistakes - avoid these", "", "- **Don't read #{schema_dump_path}** - use `get_schema`. It adds [indexed]/[unique] hints you'd miss.", "- **Don't read model files for reference** - use `get_model_details`. It resolves concerns, inherited methods, and implicit belongs_to validations.", "- **Prefer `#{search_tool}` over Grep** for method tracing and cross-layer search. It excludes sensitive files, supports `match_type:\"trace\"`, and paginates.", "- **Don't call tools without a target** - `get_model_details()` without `model:` returns a paginated list, not an error. Always specify what you want.", "- **Don't skip validation** - run `#{validate_tool}` after EVERY edit. It catches syntax errors AND Rails-specific issues (missing partials, bad column refs).", "- **Don't ignore cross-references** - tool responses include `_Next:` hints suggesting the best follow-up call. Follow them.", "- **Don't call `detail:\"full\"` first** - start with `summary` to find your target, then drill in. Full responses bury the signal.", "" ] end |
#tools_detail_guidance ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 92 def tools_detail_guidance detail_param = tool_mode == :cli ? "detail=summary" : "detail:\"summary\"" context_tool = tool_mode == :cli ? cli_cmd("context") : "rails_get_context" analyze_tool = tool_mode == :cli ? cli_cmd("analyze_feature") : "rails_analyze_feature" [ "### detail parameter - ALWAYS start with summary", "", "Individual lookup tools accept `#{detail_param}`. Use the right level:", "- **summary** - first call, orient yourself (table list, model names, route overview)", "- **standard** - working detail (columns with types, associations, action source) - DEFAULT", "- **full** - only when you need indexes, foreign keys, code snippets, or complete content", "", "Pattern: summary to find the target → standard to understand it → full only if needed.", "", "**Do NOT pass `detail` to composite tools** - `#{context_tool}` and `#{analyze_tool}` do not accept it and will return an error.", "" ] end |
#tools_header ⇒ Object
47 48 49 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 47 def tools_header "## Tools (#{tool_count}) - MANDATORY, Use Before Read" end |
#tools_intro ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 51 def tools_intro case tool_mode when :cli [ "This project has #{count_phrase(tool_count, "introspection tool")}. **MANDATORY - use these instead of reading files.**", "They return ground truth from the running app: real schema, real associations, real filters - not guesses.", "Read files ONLY when you are about to Edit them.", "" ] else [ "This project has #{count_phrase(tool_count, "MCP tool")} via `#{serve_cmd}`.", "**MANDATORY - use these instead of reading files.** They return ground truth from the running app:", "real schema, real associations, real filters - not guesses from file reads.", "Read files ONLY when you are about to Edit them.", "If MCP tools are not connected, use CLI fallback: `#{cli_cmd("TOOL_NAME", "param=value")}`", "" ] end end |
#tools_name_list ⇒ Object
Dense one-line-per-tool listing, from the same registry the table uses.
297 298 299 300 301 302 303 304 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 297 def tools_name_list all_tools = tool_rows.map { |tool, _row| tool.tool_name } [ "### All #{count_phrase(all_tools.size, "tool")}", "`#{all_tools.join('` `')}`", "" ] end |
#tools_power_tool_section ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 111 def tools_power_tool_section [ "### Start here - composite tools save multiple calls", "", "**New to this project?** Get a full walkthrough first:", tool_call("rails_onboard(detail:\"standard\")", cli_cmd("onboard", "detail=standard")), "", "**`get_context` is your power tool** - bundles schema + model + controller + routes + views in ONE call:", tool_call("rails_get_context(controller:\"PostsController\", action:\"create\")", cli_cmd("context", "controller=PostsController action=create")), tool_call("rails_get_context(model:\"Post\")", cli_cmd("context", "model=Post")), tool_call("rails_get_context(feature:\"post\")", cli_cmd("context", "feature=post")), "", "**`analyze_feature` for broad discovery** - scans all layers (models, controllers, routes, services, jobs, views, tests):", tool_call("rails_analyze_feature(feature:\"authentication\")", cli_cmd("analyze_feature", "feature=authentication")), "", "Use individual tools only when you need deeper detail on a specific layer.", "" ] end |
#tools_rules_section ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 206 def tools_rules_section case tool_mode when :cli [ "### Rules", "", "1. **Use composite tools first** - `#{cli_cmd("context")}` and `#{cli_cmd("analyze_feature")}` before individual tools", "2. **NEVER read reference files** - #{schema_dump_path}, config/routes.rb, model files, test files - tools are better", "3. **Prefer `#{cli_cmd("search_code")}`** for tracing and cross-layer search - standard search tools are fine for simple targeted lookups", "4. **Read files ONLY to Edit them** - not for reference", "5. **Validate EVERY edit** - `#{cli_cmd("validate", "files=... level=rails")}`", "6. **Follow _Next:_ hints** - tool responses suggest the best follow-up call", "" ] else [ "### Rules", "", "1. **Use composite tools first** - `rails_get_context` and `rails_analyze_feature` before individual tools", "2. **NEVER read reference files** - #{schema_dump_path}, config/routes.rb, model files, test files - tools are better", "3. **Prefer `rails_search_code`** for tracing and cross-layer search - standard search tools are fine for simple targeted lookups", "4. **Read files ONLY to Edit them** - not for reference", "5. **Validate EVERY edit** - `rails_validate(files:[...], level:\"rails\")`", "6. **Follow _Next:_ hints** - tool responses suggest the best follow-up call", "7. If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`", "" ] end end |
#tools_table ⇒ Object
236 237 238 239 240 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 236 def tools_table lines = [ "### All #{tool_count} Tools", "" ] lines.concat(build_tools_table(include_mcp: tool_mode != :cli)) lines end |
#tools_workflow_section ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 131 def tools_workflow_section [ "### Step-by-step workflows (follow this order)", "", "**Modify a model** (add field, change validation, add scope):", "1. #{tool_call_inline("rails_get_context", "model:\"Post\"", "context", "model=Post")} - schema + associations + validations in one call", "2. Read the model file, make your edit", "3. #{tool_call_inline("rails_migration_advisor", "action:\"add_column\", table:\"posts\", column:\"rating\", type:\"integer\"", "migration_advisor", "action=add_column table=posts column=rating type=integer")} - if schema change needed", "4. #{tool_call_inline("rails_validate", "files:[\"app/models/post.rb\"], level:\"rails\"", "validate", "files=app/models/post.rb level=rails")} - EVERY time after editing", "5. #{tool_call_inline("rails_generate_test", "model:\"Post\"", "generate_test", "model=Post")} - generate tests matching project patterns", "", "**Fix a controller bug:**", "1. #{tool_call_inline("rails_get_context", "controller:\"PostsController\", action:\"create\"", "context", "controller=PostsController action=create")} - action source + routes + views + model", "2. Read the controller file, make your fix", "3. #{tool_call_inline("rails_validate", "files:[\"app/controllers/posts_controller.rb\"], level:\"rails\"", "validate", "files=app/controllers/posts_controller.rb level=rails")}", "" ] + (api_only? ? api_endpoint_workflow_lines : view_workflow_lines) + [ "**Trace a method:**", tool_call("rails_search_code(pattern:\"publishable?\", match_type:\"trace\")", cli_cmd("search_code", "pattern=\"publishable?\" match_type=trace")), "", "**Debug an error (one call - gathers context + git + logs + fix):**", tool_call("rails_diagnose(error:\"NoMethodError: undefined method `foo` for nil\", file:\"app/models/post.rb\")", cli_cmd("diagnose", "error=\"NoMethodError: undefined method foo\" file=app/models/post.rb")), "", "**Review changes before merging:**", tool_call("rails_review_changes(ref:\"main\")", cli_cmd("review_changes", "ref=main")), "", "**Generate tests matching project patterns:**", tool_call("rails_generate_test(model:\"Post\")", cli_cmd("generate_test", "model=Post")), "" ] end |
#view_workflow_lines ⇒ Object
HTML/Hotwire apps get the view-editing workflow.
164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/rails_ai_context/serializers/tool_guide_helper.rb', line 164 def view_workflow_lines [ "**Build or modify a view:**", "1. #{tool_call_inline("rails_get_view", "controller:\"posts\"", "view", "controller=posts")} - existing templates, partials, Stimulus refs", "2. #{tool_call_inline("rails_get_partial_interface", "partial:\"shared/status_badge\"", "partial_interface", "partial=shared/status_badge")} - partial locals contract", "3. #{tool_call_inline("rails_get_component_catalog", "component:\"Button\"", "component_catalog", "component=Button")} - ViewComponent/Phlex props, slots, previews", "4. Read the view file, make your edit", "5. #{tool_call_inline("rails_validate", "files:[\"app/views/posts/index.html.erb\"]", "validate", "files=app/views/posts/index.html.erb")}", "" ] end |