Module: RailsAiBridge::Serializers::SharedAssistantGuidance

Defined in:
lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb

Overview

Shared high-signal copy for Copilot, Codex, Cursor, and legacy .cursorrules compact output.

See Also:

Constant Summary collapse

OMIT_MERGE_FIRST_LINE =

First line of overrides.md while in stub mode — file is not merged until removed.

/\A<!--\s*rails-ai-bridge:omit-merge\s*-->\z/i
ANTI_HALLUCINATION_HEADING =

Shared heading used by compact assistant files.

'## Anti-hallucination'

Class Method Summary collapse

Class Method Details

.anti_hallucination_rules_enabled?Boolean

Returns true when compact output should include the shared anti-hallucination block.

Returns:

  • (Boolean)

    true when compact output should include the shared anti-hallucination block



237
238
239
240
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 237

def anti_hallucination_rules_enabled?
  # archspec:disable-next-line dependencies.forbid -- FP: RailsAiBridge is the reopened gem namespace; .configuration accessor is not a cross-component dependency
  RailsAiBridge.configuration.anti_hallucination_rules
end

.anti_hallucination_rules_linesArray<String>

Returns markdown lines including heading and trailing blank line, or [] when Config::Output#anti_hallucination_rules is off.

Returns:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 21

def anti_hallucination_rules_lines
  return [] unless anti_hallucination_rules_enabled?

  [
    ANTI_HALLUCINATION_HEADING,
    '',
    '- Verify before you write (column, association, route, helper, gem).',
    '- Mark assumptions with `[ASSUMPTION]`. Silent guesses are forbidden.',
    '- This app is not average Rails. Query conventions and gems before scaffolding.',
    '- Check the inheritance chain (filters, concerns, STI) before editing a controller or model.',
    '- Empty tool output is information, not permission to invent.',
    '- Re-query after writes. Stale tool output lies.',
    ''
  ]
end

Full-mode Claude Code footer (plain-language bullets; not the compact rules list).

Parameters:

  • context (Hash)

    introspection context (+:conventions+ optional for architecture line)

Returns:

  • (Array<String>)

    markdown lines (join with newlines for a string)



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 182

def claude_full_footer_lines(context)
  arch = context.dig(:conventions, :architecture)
  arch_summary = arch&.any? ? arch.join(', ') : nil

  lines = [
    '## Behavioral Rules',
    '',
    'When working in this codebase:',
    '- Follow existing patterns and conventions detected above',
    '- Use the database schema as the source of truth for column names and types',
    '- Respect existing associations and validations when modifying models'
  ]
  lines << "- Match the project's architecture style (#{arch_summary})" if arch_summary
  lines << "- Run `#{ContextSummary.test_command(context)}` after making changes to verify correctness"
  lines.concat(RegenerationFooter.continuation_lines(command: 'rails ai:bridge', variant: :context_file))
  lines
end

Closing rules and regeneration footer (shared by compact provider serializers and Providers::RulesOrchestrator).

Parameters:

  • context (Hash)

    introspection context (+:conventions+ optional for architecture line)

  • rules_heading (String) (defaults to: '## Rules')

    markdown ## ... line opening the rules section (default: ## Rules)

  • include_anti_hallucination (Boolean) (defaults to: true)

    prepend #anti_hallucination_rules_lines (set false when the caller already included that block via #compact_engineering_rules_lines)

Returns:

  • (Array<String>)

    markdown lines



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 207

def compact_engineering_rules_footer_lines(context, rules_heading: '## Rules', include_anti_hallucination: true)
  arch = context.dig(:conventions, :architecture)
  arch_summary = arch&.any? ? arch.join(', ') : nil

  lines = []
  lines.concat(anti_hallucination_rules_lines) if include_anti_hallucination
  lines.push(
    rules_heading,
    '',
    '- **Adhere to Conventions:** Strictly follow the existing patterns and conventions outlined in this document.',
    '- **Schema as Source of Truth:** Always use the database schema as the definitive source for column names, types, and relationships.',
    '- **Respect Existing Logic:** Ensure all new code respects existing associations, validations, and service objects.',
    '- **Write Tests:** All new features and bug fixes must be accompanied by corresponding tests.'
  )
  lines << "- **Match Architecture:** Align with the project's architectural style (#{arch_summary})." if arch_summary
  lines << "- **Verify Correctness:** Run `#{ContextSummary.test_command(context)}` and `bundle exec rubocop` after making changes to ensure correctness and style adherence."
  lines << ''
  lines << '---'
  lines << RegenerationFooter.message_line(command: 'rails ai:bridge', variant: :context_file)
  lines
end

.compact_engineering_rules_linesArray<String>

Returns markdown lines including heading and trailing blank line.

Returns:

  • (Array<String>)

    markdown lines including heading and trailing blank line



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
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 38

def compact_engineering_rules_lines
  anti_hallucination_rules_lines + [
    '## Engineering rules (read first)',
    '',
    'Defaults for this codebase unless existing files clearly show a different pattern.',
    '',
    '### Controllers & strong parameters',
    '- Permit attributes explicitly; never pass raw `params` into `Model.new`, `update`, or `assign_attributes`.',
    '- Extend `permit` lists deliberately when adding fields; mirror neighboring actions in the same controller.',
    '',
    '### Authentication & authorization',
    "- Guard mutating and sensitive reads with the app's existing auth (e.g. `before_action` filters, policies). A public route does not imply public data.",
    '- Use `rails_get_controllers` for filters and `rails_get_conventions` for architecture hints when unsure.',
    '',
    '### Data access & performance',
    '- Avoid N+1: use `includes` / `preload` / `eager_load` for associations used in views or serializers.',
    '- Do not load unbounded collections: paginate list endpoints, use `find_each` in jobs, stream large exports.',
    '- Large or hot tables: check indexes before new `WHERE`/`ORDER BY`; use `rails_get_schema` before heavy queries.',
    '',
    '### Security & inputs',
    '- Treat external input as untrusted; avoid `constantize` / `send` / `eval` on user-controlled strings and raw SQL string interpolation.',
    '- Allow-list host or path for any redirect built from user input (open-redirect risk).',
    '',
    '### Testing',
    '- Prefer request or system specs for HTTP flows and integration; keep model specs tight for business rules.',
    "- Run the project's test suite after substantive edits (often `bundle exec rspec` — confirm framework via `rails_get_test_info`).",
    '',
    '### Repo-specific constraints',
    '- Hot tables, tenant/auth scoping, mandatory spec types, and internal policies belong in `config/rails_ai_bridge/overrides.md`.',
    '- Remove or replace the first-line `<!-- rails-ai-bridge:omit-merge -->` marker before that file is merged into Copilot/Codex; ' \
    'use `overrides.md.example` as a starting outline.',
    '',
    '_Regenerated files are snapshots. Re-merge team-specific performance, security, or compliance rules at the top after `rails ai:bridge`, ' \
    'or keep them in separate committed instruction files._',
    ''
  ]
end

.cursor_engineering_mdc_body_lines(show_overrides_pointer: false) ⇒ Array<String>

Condensed rules for always-on Cursor MDC (stay under ~35 lines of body).

Parameters:

  • show_overrides_pointer (Boolean) (defaults to: false)

    append pointer to config/rails_ai_bridge/overrides.md

Returns:

  • (Array<String>)

    markdown body lines (no YAML frontmatter)



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 96

def cursor_engineering_mdc_body_lines(show_overrides_pointer: false)
  lines = [
    '# Engineering essentials',
    ''
  ]
  lines.concat(anti_hallucination_rules_lines)
  lines.push(
    '- **Strong params**: permit explicitly; never mass-assign raw `params`.',
    '- **Auth**: protect mutations and sensitive reads; public route ≠ public data.',
    '- **N+1**: `includes` / `preload` / `eager_load` for associations in views and serializers.',
    '- **Bounds**: paginate HTTP lists; `find_each` / `in_batches` in jobs; no `Model.all` in requests.',
    '- **Large tables**: narrow queries; check indexes before new filters/sorts; use `rails_get_schema`.',
    '- **Security**: no `constantize`/`send`/`eval` on user input; no SQL string interpolation; allow-list redirects.',
    '- **Tests**: request/system specs for HTTP; confirm runner with `rails_get_test_info`.',
    '',
    'Generated files are **snapshots** — prefer `rails_*` MCP tools for current structure.',
    'Full engineering rules: `.github/copilot-instructions.md` or `AGENTS.md`.',
    'MCP tool reference: `rails-mcp-tools.mdc`.'
  )
  lines << 'Repo-specific performance/security: `config/rails_ai_bridge/overrides.md`.' if show_overrides_pointer
  lines << ''
  lines
end

.mergeable_override_content?(body) ⇒ Boolean

Returns false when the install stub has not been activated yet.

Parameters:

  • body (String)

    trimmed file contents

Returns:

  • (Boolean)

    false when the install stub has not been activated yet



140
141
142
143
144
145
146
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 140

def mergeable_override_content?(body)
  first = body.each_line.map(&:strip).find { |line| !line.empty? }
  return false if first.nil?
  return false if OMIT_MERGE_FIRST_LINE.match?(first)

  true
end

.overrides_file_exists_and_nonempty?Boolean

Returns whether overrides are active (merge + Cursor pointer), not placeholder-only.

Returns:

  • (Boolean)

    whether overrides are active (merge + Cursor pointer), not placeholder-only



134
135
136
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 134

def overrides_file_exists_and_nonempty?
  read_assistant_overrides != nil
end

.performance_security_and_rails_examples_linesArray<String>

Baseline bullets plus concrete Rails patterns for large data.

Returns:

  • (Array<String>)


232
233
234
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 232

def performance_security_and_rails_examples_lines
  ContextSummary.compact_performance_security_section + rails_performance_examples_lines
end

.rails_performance_examples_linesArray<String>

Concrete Rails patterns to complement generic performance bullets (gem cannot know your largest tables).

Returns:

  • (Array<String>)

    markdown lines including heading; no trailing blank line required (caller adds spacing)



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 79

def rails_performance_examples_lines
  [
    '### Rails patterns (large data & hot paths)',
    '- Never use `Model.all` (or unscoped relations) in request cycles — use `where`, `limit`, or pagination.',
    '- Background jobs: iterate with `find_each` or `in_batches` instead of `each` on large relations.',
    '- Prefer `exists?` / `count` with care on huge tables; narrow with `where` first; avoid `length` on loaded associations for big sets.',
    '- Wide rows: fetch only needed columns with `select` / `pluck` when you do not need full records.',
    '- Enable or use `strict_loading` in development/test to catch accidental N+1s early.',
    '- Before adding filters or `ORDER BY` on high-volume tables, confirm indexes via `rails_get_schema` and migrations.',
    ''
  ]
end

.read_assistant_overridesString?

Returns raw markdown body from the host app overrides file, or nil if missing, empty, or still in stub mode (first non-empty line is <!-- rails-ai-bridge:omit-merge -->).

Returns:

  • (String, nil)

    raw markdown body from the host app overrides file, or nil if missing, empty, or still in stub mode (first non-empty line is <!-- rails-ai-bridge:omit-merge -->).



122
123
124
125
126
127
128
129
130
131
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 122

def read_assistant_overrides
  path = resolved_assistant_overrides_path
  return nil unless path && File.file?(path)

  body = File.read(path).strip
  return nil if body.empty?
  return nil unless mergeable_override_content?(body)

  body
end

.repo_specific_guidance_section_linesArray<String>

Returns section to splice after stack; empty if no overrides.

Returns:

  • (Array<String>)

    section to splice after stack; empty if no overrides



166
167
168
169
170
171
172
173
174
175
176
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 166

def repo_specific_guidance_section_lines
  body = read_assistant_overrides
  return [] unless body

  [
    '## Repo-specific guidance',
    '',
    body,
    ''
  ]
end

.resolved_assistant_overrides_pathString?

Returns absolute path to overrides file if Rails app is available.

Returns:

  • (String, nil)

    absolute path to overrides file if Rails app is available



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/rails_ai_bridge/serializers/shared_assistant_guidance.rb', line 149

def resolved_assistant_overrides_path
  app = AppScope.current_app
  return nil unless app

  base = app.root.to_s
  # archspec:disable-next-line dependencies.forbid -- FP: RailsAiBridge is the reopened gem namespace; .configuration accessor is not a cross-component dependency
  cfg = RailsAiBridge.configuration
  raw = cfg.assistant_overrides_path
  if raw.nil? || raw.to_s.empty?
    File.join(base, 'config/rails_ai_bridge/overrides.md')
  else
    p = raw.to_s
    Pathname.new(p).absolute? ? p : File.join(base, p)
  end
end