Class: RailsAiContext::Tools::GetConventions

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails_ai_context/tools/get_conventions.rb

Constant Summary collapse

ARCH_LABELS =
{
  "api_only" => "API-only mode (no views/assets)",
  "hotwire" => "Hotwire (Turbo + Stimulus)",
  "graphql" => "GraphQL API (app/graphql/)",
  "grape_api" => "Grape API framework (app/api/)",
  "service_objects" => "Service objects pattern (app/services/)",
  "form_objects" => "Form objects (app/forms/)",
  "query_objects" => "Query objects (app/queries/)",
  "presenters" => "Presenters/Decorators",
  "view_components" => "ViewComponent (app/components/)",
  "stimulus" => "Stimulus controllers (app/javascript/controllers/)",
  "importmaps" => "Import maps (no JS bundler)",
  "docker" => "Dockerized",
  "kamal" => "Kamal deployment",
  "ci_github_actions" => "GitHub Actions CI",
  "concerns_models" => "Model concerns (app/models/concerns/)",
  "concerns_controllers" => "Controller concerns (app/controllers/concerns/)",
  "validators" => "Custom validators (app/validators/)",
  "policies" => "Policy objects (app/policies/)",
  "serializers" => "Serializers (app/serializers/)",
  "notifiers" => "Notifiers (app/notifiers/)",
  "phlex" => "Phlex views",
  "pwa" => "PWA (app/views/pwa/)",
  "dry_rb" => "dry-rb stack",
  "multi_tenant" => "Multi-tenancy",
  "feature_flags" => "Feature flags",
  "error_monitoring" => "Error monitoring",
  "event_driven" => "Event-driven messaging",
  "zeitwerk" => "Zeitwerk autoloading",
  "solid_queue" => "Solid Queue (jobs)",
  "solid_cache" => "Solid Cache",
  "solid_cable" => "Solid Cable"
}.freeze
PATTERN_LABELS =
{
  "sti" => "Single Table Inheritance (STI)",
  "polymorphic" => "Polymorphic associations",
  "soft_delete" => "Soft deletes (paranoia/discard)",
  "versioning" => "Model versioning/auditing",
  "state_machine" => "State machines (AASM/workflow)",
  "multi_tenancy" => "Multi-tenancy",
  "searchable" => "Full-text search (Searchkick/pg_search/Ransack)",
  "taggable" => "Tagging",
  "sluggable" => "Friendly URLs/slugs",
  "nested_set" => "Tree/nested set structures"
}.freeze

Constants inherited from BaseTool

BaseTool::DEFAULT_SESSION, BaseTool::MAX_SESSIONS, BaseTool::MAX_SESSION_ID_LENGTH, BaseTool::SESSION_CONTEXT, BaseTool::SHARED_CACHE

Class Method Summary collapse

Methods inherited from BaseTool

abstract!, abstract?, api_only_app?, api_only_note, cache_key, cached_context, config, current_session, #dedupe_put_patch_routes, detail_param?, error_response, evict_oldest_sessions, extract_method_source_from_file, extract_method_source_from_string, find_closest_match, fuzzy_find_key, guide_row, inherited, introspection_warnings_note, invalid_detail_note, normalize_detail, not_found_response, paginate, rails_app, rails_env_name, registered_tools, reset_all_caches!, reset_cache!, session_from, session_params, session_queries, session_record, session_reset!, static_tier_banner, static_tier_refusal, text_response, touch_session, unavailable_note, with_session, with_session_for

Methods included from SectionFetch

#fetch_section, usable?

Class Method Details

.call(server_context: nil) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rails_ai_context/tools/get_conventions.rb', line 21

def self.call(server_context: nil)
  fetch_section(:conventions, subject: "Convention detection") do |conventions|
    lines = [ "# App Conventions & Architecture", "" ]

    # Architecture
    if conventions[:architecture]&.any?
      lines << "## Architecture"
      conventions[:architecture].each { |a| lines << "- #{humanize_arch(a)}" }
    end

    # Patterns
    if conventions[:patterns]&.any?
      lines << "" << "## Detected patterns"
      conventions[:patterns].each { |p| lines << "- #{humanize_pattern(p)}" }
    end

    # Directory structure
    if conventions[:directory_structure]&.any?
      lines << "" << "## Directory structure"
      conventions[:directory_structure].sort_by { |k, _| k }.each do |dir, count|
        lines << "- `#{dir}/` → #{count_phrase(count, "file")}"
      end
    end

    # Frontend stack from package.json
    frontend = detect_frontend_stack
    if frontend.any?
      lines << "" << "## Frontend stack"
      frontend.each { |f| lines << "- #{f}" }
    end

    # App-specific patterns detected from controller source
    app_patterns = detect_app_patterns
    if app_patterns.any?
      lines << "" << "## App Patterns"
      app_patterns.each { |section| lines << section }
    end

    # Custom directories
    if conventions[:custom_directories]&.any?
      lines << "" << "## Custom Directories"
      conventions[:custom_directories].each do |dir, desc|
        lines << (desc.to_s.strip.empty? ? "- `#{dir}/`" : "- `#{dir}/` - #{desc}")
      end
    end

    # Config files - only show non-obvious ones (skip files every Rails app has)
    if conventions[:config_files]&.any?
      obvious = %w[
        config/application.rb config/puma.rb config/locales/en.yml
        Gemfile package.json Rakefile
      ]
      notable = conventions[:config_files].reject { |f| obvious.include?(f) }
      if notable.any?
        lines << "" << "## Notable config files"
        notable.each { |f| lines << "- `#{f}`" }
      end
    end

    # I18n / Locale info
    locale_info = detect_locale_info
    if locale_info.any?
      lines << "" << "## I18n"
      locale_info.each { |l| lines << "- #{l}" }
    end

    # Convention Fingerprint - one-paragraph summary of the app's detected conventions
    fingerprint_parts = []
    fingerprint_parts << conventions[:architecture].map { |a| humanize_arch(a) }.join(", ") if conventions[:architecture]&.any?
    fingerprint_parts << conventions[:patterns].map { |p| humanize_pattern(p) }.join(", ") if conventions[:patterns]&.any?
    top_dirs = conventions[:directory_structure]&.sort_by { |_, v| -v }&.first(5)&.map { |k, _| k }
    fingerprint_parts << "key dirs: #{top_dirs.join(', ')}" if top_dirs&.any?
    if fingerprint_parts.any?
      lines << "" << "## Convention Fingerprint"
      lines << "This app uses #{fingerprint_parts.join('; ')}."
    end

    text_response(lines.join("\n"))
  end
end