rails-ai-context 5.32.2 → 5.34.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1491 -0
- data/CONTEXT.md +27 -3
- data/README.md +16 -12
- data/Rakefile +6 -0
- data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
- data/config/routes.rb +2 -3
- data/docs/ARCHITECTURE.md +7 -5
- data/docs/CLI.md +79 -6
- data/docs/COMPATIBILITY.md +75 -1
- data/docs/CONFIGURATION.md +30 -11
- data/docs/CUSTOM_TOOLS.md +2 -1
- data/docs/FAQ.md +16 -6
- data/docs/GUIDE.md +117 -71
- data/docs/INTROSPECTORS.md +10 -8
- data/docs/QUICKSTART.md +3 -3
- data/docs/RECIPES.md +1 -1
- data/docs/SECURITY.md +109 -25
- data/docs/SETUP.md +30 -6
- data/docs/STANDALONE.md +10 -3
- data/docs/TOOLS.md +152 -34
- data/docs/TROUBLESHOOTING.md +90 -12
- data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
- data/docs/adr/0005-workspace-mcp-entries.md +39 -0
- data/exe/rails-ai-context +733 -104
- data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
- data/lib/rails_ai_context/app_kind.rb +18 -5
- data/lib/rails_ai_context/ast_cache.rb +7 -0
- data/lib/rails_ai_context/base_mixins.rb +8 -8
- data/lib/rails_ai_context/boot_manager.rb +22 -3
- data/lib/rails_ai_context/change_watch.rb +14 -11
- data/lib/rails_ai_context/cli/app_root.rb +401 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
- data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
- data/lib/rails_ai_context/code_reloader.rb +115 -1
- data/lib/rails_ai_context/concern_membership.rb +3 -1
- data/lib/rails_ai_context/concern_paths.rb +8 -3
- data/lib/rails_ai_context/configuration.rb +41 -9
- data/lib/rails_ai_context/context_file_report.rb +19 -2
- data/lib/rails_ai_context/database_file.rb +26 -0
- data/lib/rails_ai_context/doctor.rb +1122 -184
- data/lib/rails_ai_context/engine.rb +18 -1
- data/lib/rails_ai_context/erb_source.rb +19 -0
- data/lib/rails_ai_context/executable.rb +21 -0
- data/lib/rails_ai_context/facts_formatter.rb +5 -2
- data/lib/rails_ai_context/file_walk.rb +61 -10
- data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
- data/lib/rails_ai_context/fingerprinter.rb +40 -12
- data/lib/rails_ai_context/gem_lock.rb +186 -27
- data/lib/rails_ai_context/git_ignore.rb +22 -0
- data/lib/rails_ai_context/install/ai_tool.rb +31 -5
- data/lib/rails_ai_context/install/cleanup.rb +21 -0
- data/lib/rails_ai_context/install/program.rb +209 -35
- data/lib/rails_ai_context/install/selection_record.rb +258 -97
- data/lib/rails_ai_context/install/surface.rb +6 -1
- data/lib/rails_ai_context/install/validation_hook.rb +282 -0
- data/lib/rails_ai_context/install/workspace.rb +166 -0
- data/lib/rails_ai_context/install_mode.rb +71 -13
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
- data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
- data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
- data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
- data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
- data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
- data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
- data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
- data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
- data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
- data/lib/rails_ai_context/live_reload.rb +20 -20
- data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
- data/lib/rails_ai_context/mcp_edge.rb +182 -32
- data/lib/rails_ai_context/middleware.rb +5 -1
- data/lib/rails_ai_context/migration_status.rb +88 -25
- data/lib/rails_ai_context/output_guard.rb +48 -2
- data/lib/rails_ai_context/package_json.rb +1 -1
- data/lib/rails_ai_context/path_resolver.rb +46 -13
- data/lib/rails_ai_context/payload.rb +92 -8
- data/lib/rails_ai_context/pending_migrations.rb +5 -1
- data/lib/rails_ai_context/portable_path.rb +3 -2
- data/lib/rails_ai_context/presets.rb +15 -11
- data/lib/rails_ai_context/redaction.rb +262 -51
- data/lib/rails_ai_context/resources.rb +55 -20
- data/lib/rails_ai_context/route_coverage.rb +8 -0
- data/lib/rails_ai_context/safe_file.rb +14 -1
- data/lib/rails_ai_context/safe_path.rb +101 -4
- data/lib/rails_ai_context/schema_adapter.rb +8 -0
- data/lib/rails_ai_context/schema_version.rb +9 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
- data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
- data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
- data/lib/rails_ai_context/server.rb +304 -71
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
- data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
- data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
- data/lib/rails_ai_context/tools/base_tool.rb +367 -46
- data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
- data/lib/rails_ai_context/tools/diagnose.rb +218 -11
- data/lib/rails_ai_context/tools/generate_test.rb +339 -39
- data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
- data/lib/rails_ai_context/tools/get_concern.rb +91 -27
- data/lib/rails_ai_context/tools/get_config.rb +13 -4
- data/lib/rails_ai_context/tools/get_context.rb +71 -14
- data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
- data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
- data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
- data/lib/rails_ai_context/tools/get_env.rb +18 -9
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
- data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
- data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
- data/lib/rails_ai_context/tools/get_routes.rb +29 -8
- data/lib/rails_ai_context/tools/get_schema.rb +52 -19
- data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
- data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
- data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
- data/lib/rails_ai_context/tools/get_view.rb +14 -14
- data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
- data/lib/rails_ai_context/tools/onboard.rb +52 -8
- data/lib/rails_ai_context/tools/performance_check.rb +5 -3
- data/lib/rails_ai_context/tools/query.rb +914 -86
- data/lib/rails_ai_context/tools/read_logs.rb +69 -15
- data/lib/rails_ai_context/tools/review_changes.rb +215 -53
- data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
- data/lib/rails_ai_context/tools/safe_call.rb +35 -14
- data/lib/rails_ai_context/tools/search_code.rb +243 -67
- data/lib/rails_ai_context/tools/search_docs.rb +4 -4
- data/lib/rails_ai_context/tools/security_scan.rb +143 -33
- data/lib/rails_ai_context/tools/session_context.rb +39 -5
- data/lib/rails_ai_context/tools/validate.rb +259 -40
- data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +67 -40
- data/lib/rails_ai_context/view_file.rb +41 -3
- data/lib/rails_ai_context/watcher.rb +51 -13
- data/lib/rails_ai_context.rb +21 -0
- data/server.json +1 -1
- metadata +14 -1
|
@@ -9,8 +9,12 @@ module RailsAiContext
|
|
|
9
9
|
# Generated files:
|
|
10
10
|
# app/models/AGENTS.md - model listing, loaded when editing models
|
|
11
11
|
# app/controllers/AGENTS.md - controller listing, loaded when editing controllers
|
|
12
|
+
#
|
|
13
|
+
# Teams write these by hand too, so one the gem did not generate keeps
|
|
14
|
+
# its text and gets the gem's listing as a marked block.
|
|
12
15
|
class OpencodeRulesSerializer < Base
|
|
13
16
|
include StackOverviewHelper
|
|
17
|
+
include ToolGuideHelper
|
|
14
18
|
|
|
15
19
|
RULE_FILES = {
|
|
16
20
|
"app/models/AGENTS.md" => { renderer: :render_models_reference, reason: "no models" },
|
|
@@ -29,15 +33,23 @@ module RailsAiContext
|
|
|
29
33
|
if Dir.exist?(File.dirname(filepath))
|
|
30
34
|
RuleFile.new(filepath, send(rule[:renderer]), rule[:reason])
|
|
31
35
|
else
|
|
32
|
-
RuleFile.new(filepath, nil,
|
|
36
|
+
RuleFile.new(filepath, nil, missing_directory_reason(File.dirname(relative), output_dir))
|
|
33
37
|
end
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
write_rule_files(entries)
|
|
40
|
+
write_rule_files(entries, shared: true)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
private
|
|
40
44
|
|
|
45
|
+
# OpenCode reads these beside the code, so they go only into a
|
|
46
|
+
# directory that exists. Under an output_dir that is not the app, the
|
|
47
|
+
# app's own directory is there; it is the output_dir that lacks one.
|
|
48
|
+
def missing_directory_reason(dir, output_dir)
|
|
49
|
+
elsewhere = File.expand_path(output_dir) != File.expand_path(project_root)
|
|
50
|
+
elsewhere && Dir.exist?(File.join(project_root, dir)) ? "no #{dir} under output_dir" : "#{dir} not present"
|
|
51
|
+
end
|
|
52
|
+
|
|
41
53
|
def render_models_reference
|
|
42
54
|
models = Payload.models(context)
|
|
43
55
|
return nil unless models.any?
|
|
@@ -46,7 +58,7 @@ module RailsAiContext
|
|
|
46
58
|
"# ActiveRecord Models (#{models.size})",
|
|
47
59
|
"",
|
|
48
60
|
"> #{Install::Cleanup::GENERATED_NOTE}",
|
|
49
|
-
">
|
|
61
|
+
"> #{MODELS_READING}",
|
|
50
62
|
""
|
|
51
63
|
]
|
|
52
64
|
if (notice = SectionFacts.static_notice(context))
|
|
@@ -62,8 +74,9 @@ module RailsAiContext
|
|
|
62
74
|
|
|
63
75
|
assocs = SectionFacts.associations_list(data).join(", ")
|
|
64
76
|
vals = (data[:validations] || []).size
|
|
77
|
+
note = SectionFacts.table_note(Payload.section(context, :schema), data)
|
|
65
78
|
line = "- **#{name}**"
|
|
66
|
-
line += " (table: #{data[:table_name]})" if data[:table_name]
|
|
79
|
+
line += note ? " (table #{note})" : (" (table: #{data[:table_name]})" if data[:table_name]).to_s
|
|
67
80
|
line += " - #{assocs}" unless assocs.empty?
|
|
68
81
|
line += " [#{vals}v]" if vals > 0
|
|
69
82
|
lines << line
|
|
@@ -72,14 +85,23 @@ module RailsAiContext
|
|
|
72
85
|
end
|
|
73
86
|
|
|
74
87
|
lines << "- _...#{models.size - 30} more_" if models.size > 30
|
|
75
|
-
lines
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
lines.concat(tool_pointers(
|
|
89
|
+
[ "rails_get_model_details", 'model:"Name"', "model=Name", "for associations, validations, scopes, enums." ],
|
|
90
|
+
[ "rails_get_view", 'controller:"name"', "controller=name", "for view templates." ],
|
|
91
|
+
[ "rails_get_test_info", 'model:"Name"', "model=Name", "for existing model tests." ]
|
|
92
|
+
))
|
|
79
93
|
|
|
80
94
|
lines.join("\n")
|
|
81
95
|
end
|
|
82
96
|
|
|
97
|
+
# "Use <tool> for ..." lines under a blank one, for the tools the server serves.
|
|
98
|
+
def tool_pointers(*pointers)
|
|
99
|
+
served = pointers.select { |name, *| served?(name) }
|
|
100
|
+
return [] if served.empty?
|
|
101
|
+
|
|
102
|
+
[ "", *served.map { |name, mcp_params, cli_params, purpose| "Use #{tool_ref(name, mcp_params, cli_params)} #{purpose}" } ]
|
|
103
|
+
end
|
|
104
|
+
|
|
83
105
|
def render_controllers_reference
|
|
84
106
|
app_controllers = Payload.app_controllers(context)
|
|
85
107
|
return nil if app_controllers.empty?
|
|
@@ -88,7 +110,7 @@ module RailsAiContext
|
|
|
88
110
|
"# Controllers (#{app_controllers.size})",
|
|
89
111
|
"",
|
|
90
112
|
"> #{Install::Cleanup::GENERATED_NOTE}",
|
|
91
|
-
"> Read controller files directly when editing. Use
|
|
113
|
+
"> Read controller files directly when editing. Use #{tools_noun} for reference only.",
|
|
92
114
|
""
|
|
93
115
|
]
|
|
94
116
|
if (notice = SectionFacts.static_notice(context))
|
|
@@ -109,12 +131,13 @@ module RailsAiContext
|
|
|
109
131
|
jobs = job_names
|
|
110
132
|
lines << "**Jobs:** #{jobs.join(', ')}" if jobs.any?
|
|
111
133
|
|
|
112
|
-
lines
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
lines.concat(tool_pointers(
|
|
135
|
+
[ "rails_get_controllers", 'controller:"Name", action:"index"', "controller=Name action=index", "for one action's source code." ],
|
|
136
|
+
[ "rails_get_edit_context", 'file:"path", near:"keyword"', "file=path near=keyword", "for surgical edit context." ],
|
|
137
|
+
[ "rails_get_view", 'controller:"name"', "controller=name", "for view templates." ],
|
|
138
|
+
[ "rails_get_test_info", 'controller:"Name"', "controller=Name", "for existing controller tests." ],
|
|
139
|
+
[ "rails_validate", "files:[...]", "files=...", "to check syntax after editing - do NOT re-read files to verify." ]
|
|
140
|
+
))
|
|
118
141
|
|
|
119
142
|
lines.join("\n")
|
|
120
143
|
end
|
|
@@ -33,11 +33,45 @@ module RailsAiContext
|
|
|
33
33
|
models.any? ? "- Models: #{models.size}" : nil
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# The primary first, then each other database by its database.yml name:
|
|
37
|
+
# a count that leaves one out undercounts the app.
|
|
36
38
|
def database_line(ctx)
|
|
37
39
|
schema = Payload.section(ctx, :schema)
|
|
38
40
|
return nil unless schema
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
parts = [ "#{SchemaAdapter.label(ctx)} - #{CountPhrase.call(schema[:total_tables].to_i, "table")}" ]
|
|
43
|
+
Payload.secondary_databases(schema).each do |name, db|
|
|
44
|
+
tables = CountPhrase.call(db[:total_tables].to_i, "table")
|
|
45
|
+
adapter = SchemaAdapter.secondary_label(ctx, name, db)
|
|
46
|
+
parts << (framework_database_text(name, db) || "#{name}: #{adapter ? "#{adapter} - #{tables}" : tables}")
|
|
47
|
+
end
|
|
48
|
+
"- Database: #{parts.join('; ')}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A database a Rails framework keeps to itself, as every file names it:
|
|
52
|
+
# "queue: Solid Queue (13 tables)". Its tables are the framework's, so
|
|
53
|
+
# no file lists them. nil for a database of the app's own.
|
|
54
|
+
def framework_database_text(name, db)
|
|
55
|
+
framework = Payload.database_framework(db) or return nil
|
|
56
|
+
|
|
57
|
+
"#{name}: #{framework} (#{CountPhrase.call(db[:total_tables].to_i, "table")})"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The same databases, the ones with migrations of their own.
|
|
61
|
+
def migrations_line(ctx)
|
|
62
|
+
migrations = Payload.section(ctx, :migrations)
|
|
63
|
+
return nil unless migrations
|
|
64
|
+
|
|
65
|
+
pending = Payload.pending_migrations(ctx) if migrations.key?(:pending)
|
|
66
|
+
parts = [ migration_counts(migrations[:total], pending) ]
|
|
67
|
+
secondary = migrations[:secondary_databases].is_a?(Hash) ? migrations[:secondary_databases] : {}
|
|
68
|
+
secondary.each { |name, db| parts << "#{name}: #{migration_counts(db[:total], db[:pending])}" }
|
|
69
|
+
"- Migrations: #{parts.join('; ')}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# No pending count when nothing says what has been applied: zero would read as up to date.
|
|
73
|
+
def migration_counts(total, pending)
|
|
74
|
+
pending ? "#{total.to_i} total, #{pending.size} pending" : "#{total.to_i} total"
|
|
41
75
|
end
|
|
42
76
|
|
|
43
77
|
def auth_line(ctx)
|
|
@@ -78,6 +112,15 @@ module RailsAiContext
|
|
|
78
112
|
buckets
|
|
79
113
|
end
|
|
80
114
|
|
|
115
|
+
# The table a model reads when it is not plainly named: missing from a live listing, or set by
|
|
116
|
+
# a parent the static tier did not read. Nil otherwise.
|
|
117
|
+
def table_note(schema, model_data)
|
|
118
|
+
return "`#{model_data[:table_name]}` not found" if Payload.missing_table?(schema, model_data)
|
|
119
|
+
return nil if model_data[:table_name] || !model_data[:table_set_by]
|
|
120
|
+
|
|
121
|
+
"set by `#{model_data[:table_set_by]}`, which the static tier did not read"
|
|
122
|
+
end
|
|
123
|
+
|
|
81
124
|
def associations_list(model_data)
|
|
82
125
|
(model_data[:associations] || [])
|
|
83
126
|
.select { |a| a.is_a?(Hash) }
|
|
@@ -6,7 +6,8 @@ module RailsAiContext
|
|
|
6
6
|
# markers so user-added content OUTSIDE the markers survives regeneration.
|
|
7
7
|
#
|
|
8
8
|
# Used by ContextFileSerializer (for CLAUDE.md, AGENTS.md, .github/copilot-
|
|
9
|
-
# instructions.md)
|
|
9
|
+
# instructions.md), CursorRulesSerializer (for .cursorrules) and, for a
|
|
10
|
+
# per-directory AGENTS.md the user wrote, OpencodeRulesSerializer. Same
|
|
10
11
|
# marker contract everywhere - a user can hand-write content above or below
|
|
11
12
|
# the gem-managed block and it'll persist across `rails ai:context` runs.
|
|
12
13
|
module SectionMarkerWriter
|
|
@@ -20,29 +21,78 @@ module RailsAiContext
|
|
|
20
21
|
# nothing new still put a diff in a repo that commits its context files.
|
|
21
22
|
GENERATED_LINE = /^> Generated: .*\n/
|
|
22
23
|
|
|
24
|
+
BLOCK = /#{Regexp.escape(BEGIN_MARKER)}.*?#{Regexp.escape(END_MARKER)}(?:\r?\n)?/m
|
|
25
|
+
|
|
26
|
+
# Why a run left a file alone: with a marker lost, or a second block
|
|
27
|
+
# pasted in, nothing says where the gem's block ends.
|
|
28
|
+
UNPAIRED = "unpaired rails-ai-context marker, left as it is"
|
|
29
|
+
|
|
30
|
+
# Inside a file somebody else wrote, the note claims the block rather
|
|
31
|
+
# than the whole file, as Install::Cleanup::GENERATED_NOTE does.
|
|
32
|
+
BLOCK_NOTE = "This block is generated by rails-ai-context and rewritten on every run. " \
|
|
33
|
+
"Edit outside its BEGIN/END markers."
|
|
34
|
+
|
|
23
35
|
module_function
|
|
24
36
|
|
|
37
|
+
# A file under a name users write by hand too (OpenCode's
|
|
38
|
+
# app/models/AGENTS.md), rendered whole with the generated note. One
|
|
39
|
+
# that is missing, or that this gem generated whole - the note and no
|
|
40
|
+
# marker - is written whole. Any other is somebody's own, and gets the
|
|
41
|
+
# content as a block the way a root file does, so their text stays and
|
|
42
|
+
# Install::Cleanup takes out only the block.
|
|
43
|
+
def write_whole_or_block(filepath, content)
|
|
44
|
+
existing = File.exist?(filepath) ? RailsAiContext::SafeFile.read_text(filepath) : nil
|
|
45
|
+
return write_whole(filepath, content) if existing.nil? || generated_whole?(existing)
|
|
46
|
+
|
|
47
|
+
write_with_markers(filepath, content.sub(Install::Cleanup::GENERATED_LINE, "> #{BLOCK_NOTE}"))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generated_whole?(text)
|
|
51
|
+
markers(text) == :none && text.match?(Install::Cleanup::GENERATED_LINE)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A file the gem owns whole, rewritten only when it says something else.
|
|
55
|
+
# It ends with a newline, as a text file does: without one, editors and
|
|
56
|
+
# end-of-file fixers rewrote every rule file and git flagged each.
|
|
57
|
+
def write_whole(filepath, content)
|
|
58
|
+
content = "#{content}\n" unless content.end_with?("\n")
|
|
59
|
+
if File.exist?(filepath)
|
|
60
|
+
existing = RailsAiContext::SafeFile.read_text(filepath)
|
|
61
|
+
content = in_line_endings_of(existing, content)
|
|
62
|
+
return :skipped if existing == content
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
atomic_write(filepath, content)
|
|
66
|
+
:written
|
|
67
|
+
end
|
|
68
|
+
|
|
25
69
|
# Write `content` to `filepath` wrapped in markers. If the file already
|
|
26
70
|
# exists with markers, replace only the marker block (preserving user
|
|
27
71
|
# content outside). If the file exists WITHOUT markers, prepend the
|
|
28
72
|
# marker block so AI tools read our context first while keeping the
|
|
29
|
-
# user's prior content intact below.
|
|
73
|
+
# user's prior content intact below. A file whose markers do not pair
|
|
74
|
+
# up is not written: replacing a guess at the block could take the
|
|
75
|
+
# user's text, and prepending a new one duplicated the old for good.
|
|
30
76
|
#
|
|
31
77
|
# Returns :written if the file changed (created or block updated),
|
|
32
|
-
# :skipped if the file already had the exact same marker block
|
|
78
|
+
# :skipped if the file already had the exact same marker block,
|
|
79
|
+
# :unpaired if its markers do not pair up.
|
|
33
80
|
def write_with_markers(filepath, content)
|
|
34
81
|
marked_content = "#{BEGIN_MARKER}\n#{content}\n#{END_MARKER}\n"
|
|
35
82
|
|
|
36
83
|
if File.exist?(filepath)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
# Read as UTF-8 whatever the locale: a C locale tagged it US-ASCII,
|
|
85
|
+
# and the first non-ASCII character stopped the rewrite. A file that
|
|
86
|
+
# is no UTF-8 keeps its bytes, and the block joins them as bytes.
|
|
87
|
+
existing = RailsAiContext::SafeFile.read_text(filepath)
|
|
88
|
+
marked_content = in_line_endings_of(existing, marked_content)
|
|
89
|
+
marked_content = marked_content.b if existing.encoding == Encoding::BINARY
|
|
90
|
+
|
|
91
|
+
new_content = case markers(existing)
|
|
92
|
+
# A block, so a backslash in the content is not read as a backreference.
|
|
93
|
+
when :pair then existing.sub(BLOCK) { marked_content }
|
|
94
|
+
when :none then "#{marked_content}#{crlf?(existing) ? "\r\n" : "\n"}#{existing}"
|
|
95
|
+
else return :unpaired
|
|
46
96
|
end
|
|
47
97
|
|
|
48
98
|
return :skipped if same_but_for_timestamp?(existing, new_content)
|
|
@@ -55,6 +105,38 @@ module RailsAiContext
|
|
|
55
105
|
end
|
|
56
106
|
end
|
|
57
107
|
|
|
108
|
+
# :pair for one BEGIN marker before one END marker, :none for neither,
|
|
109
|
+
# :unpaired for anything else: a lone marker, two blocks, END first.
|
|
110
|
+
def markers(text)
|
|
111
|
+
begins = text.scan(BEGIN_MARKER).size
|
|
112
|
+
ends = text.scan(END_MARKER).size
|
|
113
|
+
return :none if begins.zero? && ends.zero?
|
|
114
|
+
return :pair if begins == 1 && ends == 1 && text.index(BEGIN_MARKER) < text.index(END_MARKER)
|
|
115
|
+
|
|
116
|
+
:unpaired
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Files a write's outcome under the bucket a context run reports it in.
|
|
120
|
+
def report(outcome, filepath, result)
|
|
121
|
+
case outcome
|
|
122
|
+
when :written then result[:written] << filepath
|
|
123
|
+
when :skipped then result[:skipped] << filepath
|
|
124
|
+
when :unpaired then result[:not_applicable][filepath] = UNPAIRED
|
|
125
|
+
end
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Text in the line endings of the file it goes into. A file checked out
|
|
130
|
+
# on Windows ends its lines in CRLF, and a block in LF beside them
|
|
131
|
+
# mixed the two and left a blank line under END on every run.
|
|
132
|
+
def in_line_endings_of(existing, text)
|
|
133
|
+
crlf?(existing) ? text.gsub(/\r?\n/, "\r\n") : text
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def crlf?(text)
|
|
137
|
+
text.include?("\r\n")
|
|
138
|
+
end
|
|
139
|
+
|
|
58
140
|
def same_but_for_timestamp?(existing, candidate)
|
|
59
141
|
existing == candidate ||
|
|
60
142
|
existing.gsub(GENERATED_LINE, "") == candidate.gsub(GENERATED_LINE, "")
|
|
@@ -13,6 +13,12 @@ module RailsAiContext
|
|
|
13
13
|
RuleFile = Struct.new(:path, :content, :reason)
|
|
14
14
|
|
|
15
15
|
IN_REPO_ENGINES_SHOWN = 5
|
|
16
|
+
DATABASES_SHOWN = 3
|
|
17
|
+
|
|
18
|
+
# The models listings' line on reading, in the tools guide's terms: the
|
|
19
|
+
# tools are the reference, and a file is read to edit it. "Read model
|
|
20
|
+
# files for business logic" sat beside "NEVER read ... model files".
|
|
21
|
+
MODELS_READING = "Check here first for scopes, constants, associations. Read a model file only when you edit it."
|
|
16
22
|
|
|
17
23
|
# Returns an array of summary lines for full-preset introspectors.
|
|
18
24
|
# Each line is only added if the introspector returned meaningful data.
|
|
@@ -78,7 +84,10 @@ module RailsAiContext
|
|
|
78
84
|
db_list = raw_databases.is_a?(Hash) ? raw_databases.keys : Array(raw_databases)
|
|
79
85
|
if db_list.size > 1
|
|
80
86
|
db_names = db_list.map { |d| d.is_a?(Hash) ? d[:name] : d }
|
|
81
|
-
|
|
87
|
+
# The names it leaves out are counted, as the mounted apps' are.
|
|
88
|
+
shown = db_names.first(DATABASES_SHOWN)
|
|
89
|
+
shown << "...#{db_names.size - DATABASES_SHOWN} more" if db_names.size > DATABASES_SHOWN
|
|
90
|
+
lines << "- Databases: #{db_list.size} (#{shown.join(', ')})"
|
|
82
91
|
end
|
|
83
92
|
|
|
84
93
|
components = Payload.section(ctx, :components)
|
|
@@ -116,7 +125,7 @@ module RailsAiContext
|
|
|
116
125
|
lines << SectionFacts.models_line(context)
|
|
117
126
|
|
|
118
127
|
if (routes = Payload.section(context, :routes))
|
|
119
|
-
lines << "- Routes: #{
|
|
128
|
+
lines << "- Routes: #{RouteCoverage.summary(routes)}"
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
if gems
|
|
@@ -214,28 +223,31 @@ module RailsAiContext
|
|
|
214
223
|
# A nil render means the app has nothing to put in that file. It is
|
|
215
224
|
# reported rather than dropped, so a deliberate omission never looks
|
|
216
225
|
# like a failed generation.
|
|
226
|
+
#
|
|
227
|
+
# A rules directory's names are the gem's own. `shared:` is for names
|
|
228
|
+
# users write by hand too (OpenCode's app/models/AGENTS.md): a file
|
|
229
|
+
# there the gem did not generate whole keeps its text and gets the
|
|
230
|
+
# gem's block, as a root file does.
|
|
217
231
|
# @param entries [Array<RuleFile>]
|
|
218
232
|
# @return [Hash] { written: [paths], skipped: [paths], not_applicable: { path => reason } }
|
|
219
|
-
def write_rule_files(entries)
|
|
220
|
-
|
|
221
|
-
skipped = []
|
|
222
|
-
not_applicable = {}
|
|
233
|
+
def write_rule_files(entries, shared: false)
|
|
234
|
+
result = { written: [], skipped: [], not_applicable: {} }
|
|
223
235
|
|
|
224
236
|
entries.each do |entry|
|
|
225
237
|
if entry.content.nil?
|
|
226
|
-
not_applicable[entry.path] = entry.reason || "nothing to document"
|
|
238
|
+
result[:not_applicable][entry.path] = entry.reason || "nothing to document"
|
|
227
239
|
next
|
|
228
240
|
end
|
|
229
241
|
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
outcome = if shared
|
|
243
|
+
SectionMarkerWriter.write_whole_or_block(entry.path, entry.content)
|
|
232
244
|
else
|
|
233
|
-
|
|
234
|
-
written << entry.path
|
|
245
|
+
SectionMarkerWriter.write_whole(entry.path, entry.content)
|
|
235
246
|
end
|
|
247
|
+
SectionMarkerWriter.report(outcome, entry.path, result)
|
|
236
248
|
end
|
|
237
249
|
|
|
238
|
-
|
|
250
|
+
result
|
|
239
251
|
end
|
|
240
252
|
|
|
241
253
|
def project_root
|