A generated, verified, one-line-per-model data-model map kept inside AGENTS.md (or any
Markdown file), so every agent session starts with a current picture of the schema — and
nobody has to maintain it by hand.
schema_map is the annotate pattern aimed at the
brief agents read: it reflects your Active Record models into a compact block between two
markers, regenerates that block whenever the schema is dumped, and fails your tests, your CI,
and your commits when the committed block is stale.
<!-- data-model:begin -->
_Generated by `bin/rails agents:data_model` from Active Record reflection — do not edit by hand; `bin/rails agents:data_model:check` fails when stale. Legend: `!` NOT NULL · `=v` default · `a→b` through b · `x[as]` polymorphic has_* · `x*` polymorphic belongs_to · `x[]` has_many_attached._
- **Exhibit** (exhibits) — belongs_to confirmed_by, exhibit_group; has_one case→exhibit_group; has_many chat_messages[discussable], exhibit_documents, exhibit_pages→exhibit_documents; concerns ConfirmationLock, WordReferenceable; cols approved:boolean!=false, confirmed_at:datetime, confirmed_by_id:integer, included:boolean!=true, name:string, position:integer!=0, word_reference_key:string!; unique source_range, word_reference_key; checks exhibits_confirmation_attribution_pair; owner via exhibit_group→exhibit_section.case_id
- **ExhibitPage** (exhibit_pages) — belongs_to exhibit_document; has_one case→exhibit_document; attached thumbnail; cols exhibit_document_id:integer!, included:boolean!=true, page_number:integer, rotation_degrees:integer!=0; unique exhibit_document_id+page_number; checks exhibit_pages_rotation_degrees_quarter_turn; owner via exhibit_document.case_id
<!-- data-model:end -->Each line carries, in a fixed order: associations by macro (belongs_to, has_one,
has_many, has_and_belongs_to_many), Active Storage attachments (collapsed to attached),
included concerns from your app, non-boilerplate columns (id, created_at, updated_at
omitted; lock_version kept), unique indexes with partial predicates, check constraint names,
and — when an ownership column is configured — how the model reaches it: owner case_id
(direct), owner via a.b.case_id (through belongs_to chain), owner self (the owner
table), or owner none. STI children render as (table; STI < Parent) without repeating
columns. Models from engines and gems (Active Storage, Solid Queue, …) are never listed;
temporary Active Record classes defined inside migrations are also excluded. Only application
models defined under your application root are listed.
Output is deterministic: models, associations, columns, indexes, and constraints are all sorted, so the diff of a schema change is the diff of the map.
# Gemfile
gem "schema_map", github: "fluxinc/schema_map", tag: "v0.1.2"bin/rails generate schema_map:install --ownership-column=case_idThe installer:
- writes
config/initializers/schema_map.rb; - writes
test/config/data_model_map_test.rb, a verify-only Minitest guard that fails with a readable diff and the fix command, and never rewrites the file; - writes
.githooks/pre-commit, which runs the check on every commit in the test environment afterdb:test:prepare— comparing the block against the schema the commit ships (db/schema.rb), not the developer database's migration state — and, on failure, prints the diff and the fix command. It never regenerates silently, and it will not overwrite a pre-commit hook it did not write; add the same two commands to yours; - appends the markers to the target file (creating it if absent) and fills the block.
Then enable the hook per clone — typically from bin/setup:
git config core.hooksPath .githooks| Command | Purpose |
|---|---|
bin/rails agents:data_model |
Regenerate the block. Idempotent; touches nothing outside the markers. |
bin/rails agents:data_model:check |
Verify the committed block. Exit 1 with a -committed/+generated diff when stale. Use it in CI. |
db:schema:dump — and db:schema:dump:<name> in multi-database apps — is enhanced to
regenerate the block, so db:migrate, db:rollback, db:migrate:redo, and db:prepare
keep the brief current between commits. Production is naturally inert because Rails sets
dump_schema_after_migration = false there. If the target file or its markers are missing,
the enhancement raises; run the installer.
The map is rendered from the live connection, not from db/schema.rb. To keep a
behind database from producing a wrong map, agents:data_model and agents:data_model:check
refuse with a clear message while the current environment has pending migrations
(SchemaMap::PendingMigrations; the check reports status :pending_migrations). The
db:schema:dump enhancement is the one place that regenerates regardless — the dump mirrors
the live database by definition, including after db:rollback. The guard test runs against
the test database, which Rails loads from db/schema.rb.
# config/initializers/schema_map.rb
SchemaMap.configure do |config|
config.file = "AGENTS.md" # relative to Rails.root, or absolute
config.begin_marker = "<!-- data-model:begin -->"
config.end_marker = "<!-- data-model:end -->"
config.excluded_tables = SchemaMap::Configuration::DEFAULT_EXCLUDED_TABLES # active_storage_*; fnmatch patterns
config.ownership_column = "case_id" # nil omits the owner segment
config.command = "bin/rails agents:data_model" # shown in the header and in failure messages
endThe same keys are accepted as config.schema_map.* in config/application.rb.
require "schema_map/test_helper"
class DataModelMapTest < ActiveSupport::TestCase
include SchemaMap::TestHelper
test "AGENTS.md data-model map is current" do
assert_data_model_map_current
end
endSQLite and PostgreSQL are supported through the adapter API only — no SQL strings are
generated. Check constraints are reported by name (expressions differ by adapter).
Partial-index predicates are reported as the adapter stores them; PostgreSQL normalizes
them, so generate and verify the map on the adapter your test suite runs. The gem's own
suite runs on both: bundle exec rake test (SQLite) and
SCHEMA_MAP_DATABASE_URL=postgres://localhost/schema_map_test bundle exec rake test.
A hand-maintained schema summary goes stale the first time someone forgets it — and a stale map in the standing brief is worse than none, because agents trust it. This gem keeps the map derived, never hand-maintained, and guarded at every point it could drift: on schema dump, in the test suite, in CI, and on commit.
MIT.