rails-ai-context 0.8.3 → 0.8.4
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 +7 -0
- data/CLAUDE.md +1 -1
- data/README.md +1 -1
- data/demo_script.sh +1 -1
- data/lib/rails_ai_context/fingerprinter.rb +1 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +90 -3
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08211bf465556a4185faaebc25d66fffcf4bd2e54ba335708b82c5e8d81f11f2'
|
|
4
|
+
data.tar.gz: 2ab947951eb10d48acc6d674c31cea3b2ab9104a6ca9a9f6e9c409ffa1f96a7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4757bd5de1b215938543d138b4c4a077ff7d1cda9dc25164e03251acd81afe26cd65cae9855cf879950b7a77e1f8aa78fb86e9a02d2e640e10569a9a5fc2bb45
|
|
7
|
+
data.tar.gz: b2ebb6584d2886ae60c587954fdd54687c9a23a74422c395ee3d1cec063d9c499c4094ea611f1b053100b27cdaa4a47c85da006d99354437e52b6459575aab7c
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.4] - 2026-03-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`structure.sql` support** — the schema introspector now parses `db/structure.sql` when no `db/schema.rb` exists and no database connection is available. Extracts tables, columns (with SQL type normalization), indexes, and foreign keys from PostgreSQL dump format. Prefers `schema.rb` when both exist.
|
|
13
|
+
- **Fingerprinter watches `db/structure.sql`** — file changes to `structure.sql` now trigger cache invalidation and live reload.
|
|
14
|
+
|
|
8
15
|
## [0.8.3] - 2026-03-19
|
|
9
16
|
|
|
10
17
|
### Changed
|
data/CLAUDE.md
CHANGED
data/README.md
CHANGED
|
@@ -353,7 +353,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
|
|
|
353
353
|
```bash
|
|
354
354
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
355
355
|
cd rails-ai-context && bundle install
|
|
356
|
-
bundle exec rspec #
|
|
356
|
+
bundle exec rspec # 392 examples
|
|
357
357
|
bundle exec rubocop # Lint
|
|
358
358
|
```
|
|
359
359
|
|
data/demo_script.sh
CHANGED
|
@@ -109,12 +109,24 @@ module RailsAiContext
|
|
|
109
109
|
File.join(app.root, "db", "schema.rb")
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
def structure_file_path
|
|
113
|
+
File.join(app.root, "db", "structure.sql")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Fallback: parse schema file as text when DB isn't connected.
|
|
117
|
+
# Tries db/schema.rb first, then db/structure.sql.
|
|
113
118
|
# This enables introspection in CI, Claude Code, etc.
|
|
114
119
|
def static_schema_parse
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
if File.exist?(schema_file_path)
|
|
121
|
+
parse_schema_rb(schema_file_path)
|
|
122
|
+
elsif File.exist?(structure_file_path)
|
|
123
|
+
parse_structure_sql(structure_file_path)
|
|
124
|
+
else
|
|
125
|
+
{ error: "No db/schema.rb or db/structure.sql found" }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
117
128
|
|
|
129
|
+
def parse_schema_rb(path)
|
|
118
130
|
content = File.read(path)
|
|
119
131
|
tables = {}
|
|
120
132
|
current_table = nil
|
|
@@ -138,6 +150,81 @@ module RailsAiContext
|
|
|
138
150
|
note: "Parsed from db/schema.rb (no DB connection)"
|
|
139
151
|
}
|
|
140
152
|
end
|
|
153
|
+
|
|
154
|
+
def parse_structure_sql(path) # rubocop:disable Metrics/MethodLength
|
|
155
|
+
content = File.read(path)
|
|
156
|
+
tables = {}
|
|
157
|
+
|
|
158
|
+
# Match CREATE TABLE blocks
|
|
159
|
+
content.scan(/CREATE TABLE (?:public\.)?(\w+)\s*\((.*?)\);/m) do |table_name, body|
|
|
160
|
+
next if table_name.start_with?("ar_internal_metadata", "schema_migrations")
|
|
161
|
+
|
|
162
|
+
columns = parse_sql_columns(body)
|
|
163
|
+
tables[table_name] = { columns: columns, indexes: [], foreign_keys: [] }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Match CREATE INDEX / CREATE UNIQUE INDEX
|
|
167
|
+
content.scan(/CREATE (?:UNIQUE )?INDEX (\w+) ON (?:public\.)?(\w+).*?\((.+?)\)/m) do |idx_name, table, cols|
|
|
168
|
+
col_list = cols.scan(/\w+/).first
|
|
169
|
+
tables[table]&.dig(:indexes)&.push({ name: idx_name, columns: col_list })
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Match ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY (handles multi-line)
|
|
173
|
+
content.scan(/ALTER TABLE\s+(?:ONLY\s+)?(?:public\.)?(\w+)\s+ADD CONSTRAINT.*?FOREIGN KEY\s*\((\w+)\)\s*REFERENCES\s+(?:public\.)?(\w+)\((\w+)\)/m) do |from, col, to, pk|
|
|
174
|
+
tables[from]&.dig(:foreign_keys)&.push({ from_table: from, to_table: to, column: col, primary_key: pk })
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
adapter: "static_parse",
|
|
179
|
+
tables: tables,
|
|
180
|
+
total_tables: tables.size,
|
|
181
|
+
note: "Parsed from db/structure.sql (no DB connection)"
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Parse column definitions from a CREATE TABLE body
|
|
186
|
+
def parse_sql_columns(body)
|
|
187
|
+
columns = []
|
|
188
|
+
body.each_line do |line|
|
|
189
|
+
line = line.strip.chomp(",").strip
|
|
190
|
+
next if line.empty?
|
|
191
|
+
next if line.match?(/\A(PRIMARY|CONSTRAINT|CHECK|UNIQUE|EXCLUDE|FOREIGN)\b/i)
|
|
192
|
+
|
|
193
|
+
# Match: column_name type_with_params [constraints]
|
|
194
|
+
if (match = line.match(/\A"?(\w+)"?\s+(.+)/))
|
|
195
|
+
col_name = match[1]
|
|
196
|
+
rest = match[2]
|
|
197
|
+
# Extract type: everything before NOT NULL, NULL, DEFAULT, etc.
|
|
198
|
+
col_type = rest.split(/\s+(?:NOT\s+NULL|NULL|DEFAULT|PRIMARY|UNIQUE|CONSTRAINT|CHECK)\b/i).first&.strip&.downcase
|
|
199
|
+
next unless col_type && !col_type.empty?
|
|
200
|
+
columns << { name: col_name, type: normalize_sql_type(col_type) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
columns
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def normalize_sql_type(type)
|
|
207
|
+
case type
|
|
208
|
+
when /\Ainteger\z/i, /\Aint\z/i, /\Aint4\z/i then "integer"
|
|
209
|
+
when /\Abigint\z/i, /\Aint8\z/i then "bigint"
|
|
210
|
+
when /\Asmallint\z/i, /\Aint2\z/i then "smallint"
|
|
211
|
+
when /\Acharacter varying\z/i, /\Avarchar\z/i then "string"
|
|
212
|
+
when /\Atext\z/i then "text"
|
|
213
|
+
when /\Aboolean\z/i, /\Abool\z/i then "boolean"
|
|
214
|
+
when /\Atimestamp/i then "datetime"
|
|
215
|
+
when /\Adate\z/i then "date"
|
|
216
|
+
when /\Atime\z/i then "time"
|
|
217
|
+
when /\Anumeric\z/i, /\Adecimal\z/i then "decimal"
|
|
218
|
+
when /\Afloat/i, /\Adouble/i then "float"
|
|
219
|
+
when /\Ajsonb?\z/i then "json"
|
|
220
|
+
when /\Auuid\z/i then "uuid"
|
|
221
|
+
when /\Ainet\z/i then "inet"
|
|
222
|
+
when /\Acitext\z/i then "citext"
|
|
223
|
+
when /\Aarray\z/i then "array"
|
|
224
|
+
when /\Ahstore\z/i then "hstore"
|
|
225
|
+
else type
|
|
226
|
+
end
|
|
227
|
+
end
|
|
141
228
|
end
|
|
142
229
|
end
|
|
143
230
|
end
|
data/server.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.8.
|
|
10
|
+
"version": "0.8.4",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.8.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.8.4/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|