rails-ai-context 5.35.0 → 5.35.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -0
  3. data/CONTEXT.md +37 -7
  4. data/CONTRIBUTING.md +30 -1
  5. data/README.md +9 -9
  6. data/SECURITY.md +2 -2
  7. data/docs/ARCHITECTURE.md +12 -4
  8. data/docs/CLI.md +44 -15
  9. data/docs/COMPATIBILITY.md +34 -8
  10. data/docs/CONFIGURATION.md +28 -5
  11. data/docs/CUSTOM_TOOLS.md +8 -2
  12. data/docs/FAQ.md +5 -5
  13. data/docs/GUIDE.md +33 -65
  14. data/docs/INTROSPECTORS.md +4 -4
  15. data/docs/QUICKSTART.md +1 -1
  16. data/docs/RECIPES.md +5 -5
  17. data/docs/RELEASING.md +3 -1
  18. data/docs/SECURITY.md +255 -74
  19. data/docs/SETUP.md +1 -1
  20. data/docs/STANDALONE.md +2 -2
  21. data/docs/TOOLS.md +151 -90
  22. data/docs/TROUBLESHOOTING.md +123 -8
  23. data/docs/_config.yml +1 -1
  24. data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
  25. data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
  26. data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
  27. data/docs/index.md +2 -2
  28. data/exe/rails-ai-context +24 -9
  29. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
  30. data/lib/rails_ai_context/app_databases.rb +377 -0
  31. data/lib/rails_ai_context/app_kind.rb +2 -0
  32. data/lib/rails_ai_context/cli/app_root.rb +1 -0
  33. data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
  34. data/lib/rails_ai_context/configuration.rb +15 -8
  35. data/lib/rails_ai_context/database_family/mysql_runtime.rb +170 -0
  36. data/lib/rails_ai_context/database_family/postgresql_plan.rb +124 -0
  37. data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
  38. data/lib/rails_ai_context/database_family/sqlite_runtime.rb +391 -0
  39. data/lib/rails_ai_context/database_family.rb +282 -0
  40. data/lib/rails_ai_context/database_file.rb +61 -7
  41. data/lib/rails_ai_context/database_yml.rb +7 -29
  42. data/lib/rails_ai_context/doctor.rb +15 -16
  43. data/lib/rails_ai_context/instrumentation.rb +1 -1
  44. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
  45. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
  46. data/lib/rails_ai_context/introspectors/declared_constant.rb +19 -7
  47. data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
  48. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  49. data/lib/rails_ai_context/introspectors/job_introspector.rb +7 -7
  50. data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
  51. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
  52. data/lib/rails_ai_context/introspectors/model_introspector.rb +33 -39
  53. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +33 -30
  54. data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
  55. data/lib/rails_ai_context/introspectors/performance_introspector.rb +80 -24
  56. data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
  57. data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
  58. data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
  59. data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
  60. data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
  61. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
  62. data/lib/rails_ai_context/migration_status.rb +18 -46
  63. data/lib/rails_ai_context/payload.rb +48 -1
  64. data/lib/rails_ai_context/pending_migrations.rb +3 -4
  65. data/lib/rails_ai_context/run_cache.rb +5 -0
  66. data/lib/rails_ai_context/schema_adapter.rb +13 -18
  67. data/lib/rails_ai_context/secret_columns.rb +506 -0
  68. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
  69. data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -10
  70. data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
  71. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
  72. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
  73. data/lib/rails_ai_context/server.rb +143 -29
  74. data/lib/rails_ai_context/sql_gate.rb +1007 -0
  75. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
  76. data/lib/rails_ai_context/test_helper.rb +3 -1
  77. data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
  78. data/lib/rails_ai_context/tools/base_tool.rb +71 -48
  79. data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
  80. data/lib/rails_ai_context/tools/diagnose.rb +105 -42
  81. data/lib/rails_ai_context/tools/generate_test.rb +4 -6
  82. data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
  83. data/lib/rails_ai_context/tools/get_api.rb +1 -6
  84. data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
  85. data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
  86. data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
  87. data/lib/rails_ai_context/tools/get_concern.rb +6 -6
  88. data/lib/rails_ai_context/tools/get_config.rb +189 -12
  89. data/lib/rails_ai_context/tools/get_context.rb +111 -105
  90. data/lib/rails_ai_context/tools/get_controllers.rb +8 -5
  91. data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
  92. data/lib/rails_ai_context/tools/get_engines.rb +1 -6
  93. data/lib/rails_ai_context/tools/get_env.rb +1 -6
  94. data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
  95. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
  96. data/lib/rails_ai_context/tools/get_gems.rb +1 -6
  97. data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
  98. data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
  99. data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
  100. data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
  101. data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
  102. data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
  103. data/lib/rails_ai_context/tools/get_schema.rb +10 -8
  104. data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
  105. data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
  106. data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
  107. data/lib/rails_ai_context/tools/onboard.rb +24 -24
  108. data/lib/rails_ai_context/tools/performance_check.rb +15 -2
  109. data/lib/rails_ai_context/tools/query.rb +142 -1356
  110. data/lib/rails_ai_context/tools/read_logs.rb +1 -1
  111. data/lib/rails_ai_context/tools/review_changes.rb +6 -7
  112. data/lib/rails_ai_context/tools/runtime_info.rb +125 -88
  113. data/lib/rails_ai_context/tools/safe_call.rb +70 -28
  114. data/lib/rails_ai_context/tools/search_code.rb +0 -4
  115. data/lib/rails_ai_context/tools/search_docs.rb +3 -1
  116. data/lib/rails_ai_context/tools/security_scan.rb +9 -5
  117. data/lib/rails_ai_context/tools/session_context.rb +18 -12
  118. data/lib/rails_ai_context/tools/validate.rb +1 -0
  119. data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
  120. data/lib/rails_ai_context/version.rb +1 -1
  121. data/lib/rails_ai_context.rb +4 -7
  122. data/server.json +2 -2
  123. metadata +14 -3
@@ -4,6 +4,7 @@ module RailsAiContext
4
4
  module Tools
5
5
  class GetApi < BaseTool
6
6
  tool_name "rails_get_api"
7
+ section_renderer!
7
8
  description "Returns the app's API layer: api_only mode, serialization strategy (Jbuilder templates, " \
8
9
  "serializer classes), GraphQL schema size, API versioning, rate limiting, CORS origins, pagination gems, " \
9
10
  "and OpenAPI specs. " \
@@ -18,12 +19,6 @@ module RailsAiContext
18
19
  }
19
20
  )
20
21
 
21
- guide_row(
22
- order: 30,
23
- mcp: "rails_get_api",
24
- summary: "API layer: api_only mode, serializers, GraphQL, versioning, rate limiting, CORS, pagination"
25
- )
26
-
27
22
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
28
23
 
29
24
  # The name each section key goes by in the one-liner.
@@ -4,6 +4,7 @@ module RailsAiContext
4
4
  module Tools
5
5
  class GetAutoload < BaseTool
6
6
  tool_name "rails_get_autoload"
7
+ section_renderer!
7
8
  description "Get autoloading setup: Zeitwerk vs Classic mode, autoloaders with collapsed/ignored dirs, autoload/eager-load paths, and custom inflections. " \
8
9
  "Use when: debugging NameError/unloaded-constant issues, naming a file or class to match Zeitwerk rules, or checking custom inflections."
9
10
 
@@ -13,12 +14,6 @@ module RailsAiContext
13
14
 
14
15
  input_schema(properties: {})
15
16
 
16
- guide_row(
17
- order: 43,
18
- mcp: "rails_get_autoload",
19
- summary: "Zeitwerk mode, autoload/eager-load paths, collapsed dirs, custom inflections"
20
- )
21
-
22
17
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
23
18
 
24
19
  def self.call(server_context: nil)
@@ -90,14 +90,15 @@ module RailsAiContext
90
90
  callbacks = data[:callbacks] || {}
91
91
  association_lines = association_callback_lines(data)
92
92
  if callbacks.empty? && association_lines.empty?
93
- return "# #{name}\n\nNo callbacks defined.\n\n_Next: `rails_get_model_details(model:\"#{name}\")` for full model detail._"
93
+ hint = next_hint(pointer("rails_get_model_details", %(model:"#{name}"), "for full model detail."))
94
+ return [ "# #{name}", "No callbacks defined.", hint ].compact.join("\n\n")
94
95
  end
95
96
 
96
97
  lines = [ "# #{name} - Callbacks", "" ]
97
98
  if callbacks.empty?
98
99
  lines << "No record callbacks defined."
99
100
  lines.concat(association_section(association_lines))
100
- lines << "" << "_Next: `rails_get_model_details(model:\"#{name}\")` for associations and validations_"
101
+ add_next_hint(lines, pointer("rails_get_model_details", %(model:"#{name}"), "for associations and validations"))
101
102
  return lines.join("\n")
102
103
  end
103
104
 
@@ -150,9 +151,8 @@ module RailsAiContext
150
151
  lines.concat(association_section(association_lines))
151
152
 
152
153
  # Cross-reference hints
153
- lines << ""
154
- lines << "_Next: `rails_get_model_details(model:\"#{name}\")` for associations and validations"
155
- lines << " | `rails_get_concern(name:\"ConcernName\")` for concern-provided callbacks_"
154
+ add_next_hint(lines, pointer("rails_get_model_details", %(model:"#{name}"), "for associations and validations"),
155
+ pointer("rails_get_concern", %(name:"ConcernName"), "for concern-provided callbacks"), lines: true)
156
156
 
157
157
  lines.join("\n")
158
158
  end
@@ -28,7 +28,7 @@ module RailsAiContext
28
28
  )
29
29
 
30
30
  guide_row(
31
- order: 25,
31
+ order: 22,
32
32
  mcp: "rails_get_component_catalog(component:\"X\")",
33
33
  cli_args: "component=X",
34
34
  summary: "ViewComponent/Phlex: props, slots, previews, usage"
@@ -66,12 +66,11 @@ module RailsAiContext
66
66
  note = api_only_note("app/components", dir: "app/components")
67
67
  return text_response(note) if note
68
68
 
69
- return text_response(
70
- "No components found in app/components/.\n\n" \
71
- "This app may use ERB partials instead of ViewComponent/Phlex. Try:\n" \
72
- "- `rails_get_partial_interface(partial:\"shared/partial_name\")` - partial locals contract + usage\n" \
73
- "- `rails_get_view(controller:\"name\")` - view templates with partial/Stimulus references"
74
- )
69
+ tries = [ pointer("rails_get_partial_interface", %(partial:"shared/partial_name"), "- partial locals contract + usage"),
70
+ pointer("rails_get_view", %(controller:"name"), "- view templates with partial/Stimulus references") ].compact
71
+ text = "No components found in app/components/."
72
+ text += "\n\nThis app may use ERB partials instead of ViewComponent/Phlex. Try:\n#{tries.map { |t| "- #{t}" }.join("\n")}" if tries.any?
73
+ return text_response(text)
75
74
  end
76
75
  text_response(render_catalog(components, data[:summary], detail, offset: offset, limit: limit, bases: data[:bases]))
77
76
  end
@@ -230,7 +230,7 @@ module RailsAiContext
230
230
  lines << "" << "_No model or concern in app/models wires this validator._"
231
231
  end
232
232
 
233
- lines << "" << "_Next: `rails_search_code(pattern:\"#{name.demodulize.camelize}\")` for every use_"
233
+ add_next_hint(lines, pointer("rails_search_code", %(pattern:"#{name.demodulize.camelize}"), "for every use"))
234
234
  return text_response(lines.join("\n"))
235
235
  end
236
236
 
@@ -244,15 +244,15 @@ module RailsAiContext
244
244
  end
245
245
 
246
246
  # Cross-reference hints
247
- lines << ""
248
- case concern_type
247
+ hint = case concern_type
249
248
  when "model"
250
- lines << "_Next: `rails_get_model_details(model:\"ModelName\")` for models using this concern_"
249
+ next_hint(pointer("rails_get_model_details", %(model:"ModelName"), "for models using this concern"))
251
250
  when "controller"
252
- lines << "_Next: `rails_get_controllers(controller:\"ControllerName\")` for controllers using this concern_"
251
+ next_hint(pointer("rails_get_controllers", %(controller:"ControllerName"), "for controllers using this concern"))
253
252
  else
254
- lines << "_Next: `rails_search_code(pattern:\"include #{name.demodulize.camelize}\")` for everything using this concern_"
253
+ next_hint(pointer("rails_search_code", %(pattern:"include #{name.demodulize.camelize}"), "for everything using this concern"))
255
254
  end
255
+ lines << "" << hint if hint
256
256
 
257
257
  text_response(lines.join("\n"))
258
258
  end
@@ -1,24 +1,200 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module RailsAiContext
4
6
  module Tools
5
7
  class GetConfig < BaseTool
6
8
  tool_name "rails_get_config"
7
- description "Get Rails app configuration: cache store, session store, timezone, queue adapter, custom middleware, initializers. " \
8
- "Use when: configuring caching, checking session/queue setup, or seeing what initializers exist. " \
9
- "No parameters needed. Returns non-default middleware and all initializers."
10
9
 
11
- input_schema(properties: {})
10
+ # One part of the answer. `tool_name` is the tool that answered it, a retired
11
+ # name for every section but app; skip_tools turns the section off by it.
12
+ Section = Data.define(:name, :tool_name, :params, :summary) do
13
+ def app?
14
+ name == "app"
15
+ end
16
+
17
+ # The class that renders it; the app section is GetConfig's own.
18
+ def renderer
19
+ BaseTool.tool_class(tool_name)
20
+ end
21
+
22
+ def on?(config = RailsAiContext.configuration)
23
+ !config.skip_tools.include?(tool_name)
24
+ end
25
+
26
+ # On, and rails_get_config is the gem's own tool, with no custom one in its place.
27
+ def served?(config = RailsAiContext.configuration)
28
+ on?(config) && RailsAiContext::Server.served_tool("rails_get_config", config).equal?(GetConfig)
29
+ end
30
+
31
+ def off_message
32
+ "The `#{name}` section of rails_get_config is turned off in this app: skip_tools lists #{tool_name} " \
33
+ "(.rails-ai-context.yml or config/initializers/rails_ai_context.rb). Take it out of skip_tools to use it."
34
+ end
35
+ end
36
+
37
+ SECTIONS = [
38
+ Section.new(name: "app", tool_name: "rails_get_config", params: [],
39
+ summary: "database, auth, assets, cache, session, queue, Action Cable, middleware, initializers"),
40
+ Section.new(name: "gems", tool_name: "rails_get_gems", params: %i[category offset limit], summary: "notable gems by category"),
41
+ Section.new(name: "frontend", tool_name: "rails_get_frontend_stack", params: %i[detail],
42
+ summary: "JS framework, build tool, TypeScript, package manager"),
43
+ Section.new(name: "api", tool_name: "rails_get_api", params: %i[detail],
44
+ summary: "serializers, GraphQL, versioning, CORS, rate limiting, pagination"),
45
+ Section.new(name: "engines", tool_name: "rails_get_engines", params: [], summary: "mounted engines and Rack apps"),
46
+ Section.new(name: "conventions", tool_name: "rails_get_conventions", params: [], summary: "architecture and patterns"),
47
+ Section.new(name: "autoload", tool_name: "rails_get_autoload", params: [], summary: "Zeitwerk paths, inflections"),
48
+ Section.new(name: "active_support", tool_name: "rails_get_active_support", params: [],
49
+ summary: "concerns, notifications, on_load hooks"),
50
+ Section.new(name: "env", tool_name: "rails_get_env", params: %i[detail],
51
+ summary: "names of environment variables and credentials keys"),
52
+ Section.new(name: "env_config", tool_name: "rails_get_env_config", params: %i[environment offset limit],
53
+ summary: "config/environments/*.rb per environment"),
54
+ Section.new(name: "i18n", tool_name: "rails_get_i18n", params: %i[locale offset limit], summary: "locales, locale files, coverage")
55
+ ].freeze
56
+
57
+ def self.section_named(name)
58
+ SECTIONS.find { |section| section.name == name.to_s.downcase }
59
+ end
60
+
61
+ # The section a retired tool name stands for, or nil.
62
+ def self.retired_section(tool_name)
63
+ SECTIONS.find { |section| !section.app? && section.tool_name == tool_name.to_s }
64
+ end
65
+
66
+ def self.sections_on(config = RailsAiContext.configuration)
67
+ SECTIONS.select { |section| section.on?(config) }
68
+ end
69
+
70
+ # Each parameter's schema, less the description that names its sections.
71
+ PARAM_SCHEMAS = {
72
+ detail: [ {}, "summary, standard (default) or full." ],
73
+ category: [
74
+ { type: "string", enum: Introspectors::GemIntrospector::NOTABLE_GEMS.values.map { |gem| gem[:category].to_s }.uniq + %w[all] },
75
+ "only this category. Default: all."
76
+ ],
77
+ environment: [ { type: "string" }, "only this environment (e.g. \"production\")." ],
78
+ locale: [ { type: "string" }, "only this locale (e.g. \"fr\")." ],
79
+ offset: [ { type: "integer" }, "skip this many entries. Default: 0." ],
80
+ limit: [ { type: "integer" }, "max entries. Default: 50." ]
81
+ }.freeze
12
82
 
13
- guide_row(
14
- order: 21,
15
- mcp: "rails_get_config",
16
- summary: "Database adapter, auth, assets, cache, queue, Action Cable"
17
- )
83
+ # The schema over the given sections: a parameter none of them reads is
84
+ # left out, and `section` is required once app, its default, is not among them.
85
+ def self.schema_for(sections, any_section: false)
86
+ default = sections.any?(&:app?)
87
+ about = default ? "Default: app." : "Required: the app section is turned off in this app."
88
+ properties = { section: { type: "string", enum: sections.map(&:name), description: "Which section to answer. #{about}" } }
89
+ properties[:section].delete(:enum) if any_section
90
+ PARAM_SCHEMAS.each do |param, (schema, text)|
91
+ readers = sections.select { |section| section.params.include?(param) }
92
+ next if readers.empty?
93
+
94
+ text = "#{readers.map(&:name).join(', ')}: #{text}"
95
+ properties[param] = param == :detail ? RailsAiContext::DetailLevel.schema(text) : schema.merge(description: text)
96
+ end
97
+ default ? { properties: properties } : { properties: properties, required: [ "section" ] }
98
+ end
99
+
100
+ def self.description_for(sections)
101
+ listed = sections.map { |section| "#{section.name} (#{'default: ' if section.app?}#{section.summary})" }
102
+ "Get the app's configuration, one section per call. section#{' (required)' if sections.none?(&:app?)}: #{listed.join(', ')}."
103
+ end
104
+
105
+ # The schema that validates a call takes any section and every parameter, so `call` gives
106
+ # the one refusal for a section that is off or unknown; tools/list publishes the sections that are on.
107
+ VALIDATING_SCHEMA = ArgumentSchema.new(schema_for(SECTIONS, any_section: true))
108
+ description(description_for(SECTIONS))
109
+
110
+ def self.input_schema(*args)
111
+ args.empty? ? VALIDATING_SCHEMA : super
112
+ end
113
+
114
+ def self.input_schema_value
115
+ MCP::Tool::InputSchema.new(schema_for(sections_on))
116
+ end
117
+
118
+ def self.description_value
119
+ description_for(sections_on)
120
+ end
121
+
122
+ # The row calls the default section, or the first one on when app is off.
123
+ def self.guide_row
124
+ app, others = sections_on.partition(&:app?)
125
+ names = others.map(&:name).join(", ")
126
+ summary = [ ("App config (database, auth, assets, cache, queue)" if app.any?),
127
+ ("`section` picks #{app.any? ? 'another' : 'one'}: #{names}" unless names.empty?) ].compact.join("; ")
128
+ first = others.first unless app.any?
129
+ GuideRow.new(order: 20, mcp: first ? %(rails_get_config(section:"#{first.name}")) : "rails_get_config",
130
+ cli_args: ("section=#{first.name}" if first), summary: summary)
131
+ end
18
132
 
19
133
  annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
20
134
 
21
- def self.call(server_context: nil)
135
+ def self.call(section: nil, detail: nil, category: nil, offset: nil, limit: nil, environment: nil, locale: nil, server_context: nil)
136
+ on = sections_on.map(&:name).join(", ")
137
+ return error_response(RailsAiContext::Server.skipped_tool_message(tool_name)) if on.empty?
138
+
139
+ asked = section_named(section.nil? ? "app" : section)
140
+ return error_response("`section` does not take '#{echo_input(section)}'. It takes: #{on}.") unless asked
141
+ return error_response(section.nil? ? section_required_message : "#{asked.off_message} The sections that are on: #{on}.") unless asked.on?
142
+
143
+ given = { detail: detail, category: category, offset: offset, limit: limit, environment: environment, locale: locale }.compact
144
+ response = asked.app? ? app_section : render(asked, given)
145
+ append_note(response, unread_note(asked.name, given.keys - asked.params))
146
+ end
147
+
148
+ # A retired name's answer: its section's, ending with the call to make
149
+ # instead, which carries the arguments the section reads. A refusal ends as it is.
150
+ def self.with_retired_note(response, section, arguments)
151
+ response.error? ? response : append_note(response, retired_note(section, arguments))
152
+ end
153
+
154
+ # The refusal of a parameter no section reads in a retired name's call, naming its own section's; or nil.
155
+ def self.retired_refusal(section, arguments)
156
+ unknown = unknown_param_names(arguments.keys, input_schema.to_h[:properties])
157
+ unknown_param_response(unknown, section.params.map(&:to_s)) if unknown.any?
158
+ end
159
+
160
+ # A value goes into the call only when it reads the same in a code span, an MCP call and a shell command.
161
+ CALL_SAFE = /\A[\w.-]+\z/
162
+
163
+ # Every other value, and one too long to echo whole, is named after the call.
164
+ def self.retired_note(section, arguments)
165
+ spanned, outside = arguments.slice(*section.params).partition do |_, value|
166
+ value.is_a?(Integer) || value == true || value == false || (value.is_a?(String) && value.match?(CALL_SAFE) && echo_input(value) == value)
167
+ end
168
+ read = spanned.map { |key, value| "#{key}:#{JSON.generate(value)}" }.join(", ")
169
+ call = pointer(section.tool_name, read)
170
+ same = ", with the same #{outside.map { |key, _| "`#{key}`" }.join(' and ')}" if outside.any?
171
+ cli_form("\n\n_#{section.tool_name} is now #{call}#{same}; call that instead._") if call
172
+ end
173
+
174
+ # The renderer answers as a tool another tool called, so an answered call is recorded here.
175
+ private_class_method def self.render(section, given)
176
+ section.renderer.call(**given.slice(*section.params)).tap { |response| record_call(response_text(response)) unless response.error? }
177
+ end
178
+
179
+ private_class_method def self.section_required_message
180
+ "`section` is required in this app: skip_tools lists rails_get_config, which turns the default `app` section off. " \
181
+ "It takes: #{sections_on.map(&:name).join(', ')}."
182
+ end
183
+
184
+ # Only a detail the caller sent is normalized, so a section that does
185
+ # not read it can say so.
186
+ def self.normalize_detail(kwargs)
187
+ kwargs[:detail].nil? ? kwargs : super
188
+ end
189
+
190
+ private_class_method def self.unread_note(section, unread)
191
+ return nil if unread.empty?
192
+
193
+ names = unread.map { |name| "`#{name}`" }.join(", ")
194
+ "\n\n_The `#{section}` section does not read #{names}, so #{unread.size > 1 ? 'they were' : 'it was'} ignored._"
195
+ end
196
+
197
+ private_class_method def self.app_section
22
198
  if static_refusal_for(:config) && (rackup = rackup_lines).any?
23
199
  return text_response(([ unavailable_text ] + rackup).join("\n"))
24
200
  end
@@ -197,8 +373,9 @@ module RailsAiContext
197
373
  env = Rails.configuration.database_configuration&.dig(Rails.env) rescue nil
198
374
  return nil unless env.is_a?(Hash)
199
375
 
200
- # A multi-database environment nests one block per database; primary is the app's own.
201
- env["adapter"] || (env["primary"] || env.values.find { |v| v.is_a?(Hash) })&.dig("adapter")
376
+ # A multi-database environment nests one block per database; the first is the app's own.
377
+ adapter = env["adapter"] || env.values.find { |v| v.is_a?(Hash) }&.dig("adapter")
378
+ adapter && SchemaAdapter.display(adapter.to_s)
202
379
  end
203
380
 
204
381
  # The auth section's rule, read from the files when the section did not