Skip to content

Phase 2a: Complete core extraction + integration test scaffolding (0.5.0)#9

Merged
antarr merged 22 commits into
mainfrom
feature/mysql-genius-core-phase-2a
Apr 11, 2026
Merged

Phase 2a: Complete core extraction + integration test scaffolding (0.5.0)#9
antarr merged 22 commits into
mainfrom
feature/mysql-genius-core-phase-2a

Conversation

@antarr

@antarr antarr commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the core-library extraction work deferred from Phase 1b and adds integration test scaffolding that would have caught the two Phase 1b latent regressions. Zero observable behavior change for Rails host apps. Ships as mysql_genius-core 0.5.0 + mysql_genius 0.5.0 in lockstep under one v0.5.0 tag.

See docs/superpowers/specs/2026-04-11-phase-2a-core-extraction-design.md for the full design and docs/superpowers/plans/2026-04-11-phase-2a-core-extraction.md for the 26-task implementation plan.

What's included

Stage A — Integration test scaffolding (Tasks 1-9)

  • spec/dummy/ minimal Rails engine dummy app (boots in <100ms, loads only action_controller/railtie)
  • spec/rails_helper.rb + spec/support/fake_connection.rb with stub_connection / fake_column helpers
  • Request specs at spec/requests/mysql_genius/ covering every route in config/routes.rb (40+ examples)
  • Dedicated regression specs at spec/regressions/ pinning the two Phase 1b latent bugs
  • CLAUDE.md relaxed to a two-tier testing model

Stage B — Core additions, no deletions (Tasks 10-18)

  • Core::Ai::Config#domain_context field (default empty string)
  • Core::Ai::SchemaContextBuilder — shared helper for :basic / :with_cardinality schema descriptions
  • Core::Analysis::Columns — service class with tagged :ok/:blocked/:not_found result struct
  • Core::Ai::{DescribeQuery, SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk} — 5 AI prompt builder classes extracted from the Rails AiFeatures concern
  • MysqlGenius::Core.views_path module method returning the shared templates directory

Stage C — Rails adapter rewiring (Tasks 19-20)

  • QueriesController#columns delegates to Core::Analysis::Columns; 0.4.1 masked_column? helper deleted
  • 5 AI concern actions delegate to Core::Ai::* classes; build_schema_for_query helper deleted; ai_domain_context simplified
  • anomaly_detection and root_cause stay Rails-side (Redis dependency)

Stage D — ERB template migration (Tasks 21-22)

  • SharedViewHelpers concern providing path_for / render_partial via helper_method
  • Engine registers Core.views_path via initializer before: :add_view_paths
  • 11 template files moved from app/views/mysql_genius/queries/ to gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/
  • index.html.erb renamed to dashboard.html.erb
  • 18 mysql_genius.*_pathpath_for(:*) rewrites
  • 10 render "..."render_partial(:*) rewrites
  • Rails::VERSION::MAJOR/MINOR@framework_version_major/minor ivars

Stage E — Release prep (Task 23)

  • Version bumps: both gems to 0.5.0
  • Gemspec dep bump: ~> 0.4.0~> 0.5.0
  • CHANGELOG flips in both gems

Bugs caught during implementation

Stage A's tests-first approach found three Phase 1a/1b-class latent boot-order bugs before any refactoring began:

  1. rails_helper.rb required explicit require "rails"; require "mysql_genius/engine".rspec's --require spec_helper auto-loads spec_helper which requires mysql_genius when Rails isn't defined yet, so lib/mysql_genius.rb's require "mysql_genius/engine" if defined?(Rails) silently skips and Engine is never defined. Caught in Task 3.

  2. spec/dummy/config/boot.rb needed ENV["RAILS_ENV"] ||= "test" — Rails 8.1 defaults to development mode without this, and development-mode CSRF protection kicks in on POST requests causing 422 CSRF errors. Caught in Task 5.

  3. .rubocop.yml needed RSpec/VerifiedDoubleReference: Enabled: false — rubocop auto-correct was changing instance_double("ActiveRecord::Result") to a constant-form reference, but the constant isn't autoloaded at spec time. Caught in Task 5.

Plan vs reality — notable deviations

  • Templates are in core/views/mysql_genius/queries/ subdirectory (NOT flat at core/views/) because Rails prepends the controller namespace during template lookup. Phase 2b's sidecar render_partial will need to handle this layout.
  • Engine view-path initializer needs before: :add_view_paths — Rails captures view paths during a specific phase.
  • SharedViewHelpers#render_partial uses view_context.render(partial: ...) instead of plain render(...) because the latter hits ActionController#render (full response cycle) not ActionView's partial renderer.
  • ai_domain_context helper was NOT fully deleted — kept as a thin shim returning ai_system_context only, because anomaly_detection and root_cause still reference it. The hardcoded Rails-FK guidance moved into RAILS_DOMAIN_CONTEXT constant used by the 5 extracted builders. Those 2 Rails-side actions drop the guidance from their prompts (acceptable — both are runtime diagnostics, not schema recommendations).
  • 18 rubocop offenses in Stage A files (Tasks 1-2) were auto-fixed via git rebase --autosquash at the Task 9 checkpoint (my per-task rubocop SPEC_FILE calls only checked the new file each task touched).

Test plan

  • Unit specs: 76/0 adapter, 193/0 core
  • Integration specs: included in adapter 76/0 (40+ request/regression examples)
  • Rubocop: 98 files clean adapter, 60 files clean core
  • No Co-Authored-By: Claude trailer in any commit
  • Real host-app smoke test via path dep — please verify before merge. Point your epitome Gemfile at this branch's working tree, boot, exercise every tab (Dashboard, Slow Queries, Query Stats, Server, Tables, Unused Indexes, Duplicate Indexes, Query Explorer with a table selection, AI Tools), confirm zero regressions. This branch has 22 commits of Rails adapter + core gem changes, including the largest surface-area change of the project so far (the ERB migration).
  • After merge: tag v0.5.0, monitor publish workflow, verify both gems at 0.5.0 on RubyGems
  • After release: fresh bundle install against gem "mysql_genius", "0.5.0" resolves mysql_genius-core 0.5.0 transitively

What's next

  • Phase 2b — build gems/mysql_genius-desktop/ Sinatra sidecar as the first non-Rails consumer of mysql_genius-core 0.5.0. Will need to implement its own path_for / render_partial that handle the core/views/mysql_genius/queries/ namespaced layout.

antarr added 22 commits April 11, 2026 11:45
Enables integration testing against a real Rails::Application
with real routing and request dispatch. Loads only
action_controller/railtie — no ActiveRecord, no ActionMailer,
no asset pipeline. Boots in under 100ms.

Subsequent tasks add spec/rails_helper.rb and request specs
that boot this dummy and dispatch HTTP requests at the mounted
engine via Rack::Test.
rails_helper boots the spec/dummy/ app and provides Rack::Test
for request dispatch. Unit specs continue to require
"spec_helper" (no Rails boot); integration specs will require
"rails_helper" starting with the next task's request specs.

FakeConnectionHelper provides `stub_connection(tables:, columns_for:,
exec_query:, select_value:)` for configuring the AR::Base.connection
double each request spec needs. The helper builds a predictable
double so specs don't have to repeat 20 lines of stub setup.
First integration spec. Verifies the root action returns 200,
renders HTML with the expected tab markup, respects blocked_tables
when filtering the table dropdown, and handles featured_tables.

Boots spec/dummy/ Rails app via rails_helper and dispatches
via Rack::Test. AR::Base.connection is stubbed via FakeConnectionHelper.

Also fixes a boot-order bug in rails_helper: .rspec's --require spec_helper
causes spec_helper to load mysql_genius before Rails is defined, so engine.rb's
`if defined?(Rails)` guard skips loading MysqlGenius::Engine. By the time the
dummy app boots, mysql_genius is already cached and require is a no-op —
Engine is never defined. Fix: explicitly require "rails" and
"mysql_genius/engine" at the top of rails_helper before booting the dummy app.

Current 0.4.1 code passes all 4 examples — this spec documents
existing behavior as a baseline for Phase 2a refactoring.
Six examples covering every branch of the columns action:
- Happy path: JSON column metadata for a known table
- Masked column filter: password/token columns excluded
- default_columns config: :default flag correctly computed
- Blocked table: 403 Forbidden with error message
- Missing table: 404 Not Found with error message
- Regression guard: does not raise NoMethodError (the 0.4.1 bug)

The last example is the explicit pin: if someone in the future
removes the masked_column? helper from QueriesController again,
this spec fires before the code ships.
Covers happy path, SQL validation rejection (non-SELECT), blocked
tables, and a regression guard for the 0.4.0 boot-order bug that
prevented Core::Connection::ActiveRecordAdapter from being defined
at runtime.

Also fixes spec/dummy/config/boot.rb to set RAILS_ENV=test so that
the test environment config (including allow_forgery_protection=false)
is applied when running POST request specs under Rails 8.1+.

Adds RSpec/VerifiedDoubleReference: Enabled: false to .rubocop.yml
since ActiveRecord::Result is not loaded in the spec environment and
string-form instance_double references are required.
One example per route plus a consolidated boot-order regression
guard covering all six. Keeps the analysis logic unit-tested in
core and the integration suite focused on routing/dispatch/JSON
serialization concerns.
Covers every AI POST route with a stubbed Core::Ai::Client
returning canned JSON. Focuses on routing/dispatch, config gating
(ai not configured → 404), and input validation (blank params → 422).
The actual prompt construction is tested in core once the AI
builders are extracted in Stage B.
spec/regressions/phase_1b_latent_bugs_spec.rb holds explicit pins
for both bugs so they can never silently come back:

1. Core::Connection::ActiveRecordAdapter is defined after
   \`require "mysql_genius"\` (0.4.0 boot-order bug, fixed in
   3272a80).

2. GET /mysql_genius/columns does not raise NoMethodError on
   masked_column? for a valid non-blocked table (0.4.1 bug,
   fixed in 27d4662).

Lives outside spec/requests/ so it's semantically clear these are
regression pins, not functional behavior specs.
New optional keyword-init field, empty string default. Will be
interpolated into the system prompts of the extracted AI builder
classes (Stage B). Rails adapter will default it to the current
"Ruby on Rails application, don't recommend FKs" string in
Stage C; Phase 2b's sidecar will default to empty.

Two new spec examples cover the default value and explicit
assignment.
Shared helper for formatting table schema descriptions for AI
prompt context. Takes a Core::Connection; #call(tables, detail:)
returns a formatted multi-line string with table name, row count,
primary key, columns, and indexes. detail: :with_cardinality adds
information_schema.STATISTICS per-index cardinality.

Consolidates the ~10 lines of schema description logic currently
duplicated across 4 AI features in the Rails adapter's AiFeatures
concern. SchemaReview, RewriteQuery, IndexAdvisor, and
MigrationRisk (added in subsequent tasks) all compose this class.
Takes a Core::Connection plus config (blocked_tables,
masked_column_patterns, default_columns) and returns a tagged
Result struct with :ok / :blocked / :not_found status. Uses
Core::SqlValidator.masked_column? for the masked-column filter.

Matches the Phase 1b Core::Analysis::* pattern. The Rails adapter's
QueriesController#columns action will be rewritten in Stage C to
delegate to this class and map result.status to HTTP codes.
Phase 2b's sidecar will call the same class with a TrilogyAdapter.

The 0.4.1 masked_column? helper on QueriesController becomes
unnecessary once this lands — deleted in Stage C.
Pure-function prompt builder for the "describe this SQL query"
feature. Takes (client, config); #call(sql) returns whatever
Core::Ai::Client#chat returns. Interpolates config.domain_context
into the system prompt.

First of 5 extracted AI prompt builders. The remaining 4
(SchemaReview, RewriteQuery, IndexAdvisor, MigrationRisk) follow
the same pattern but take a Core::Connection for schema context
lookup via SchemaContextBuilder.
Second of 5 extracted AI prompt builders. Reviews a specific table
or the top 20 queryable tables for anti-patterns. Uses
SchemaContextBuilder to format the schema description. Mirrors the
pattern from DescribeQuery; adds a Core::Connection dependency.
Third of 5 extracted AI prompt builders. Extracts table references
from the SQL via SqlValidator.extract_table_references, builds a
SchemaContextBuilder for them, and sends the formatted schema +
the SQL to Core::Ai::Client.
Fourth of 5 extracted AI prompt builders. Takes SQL + EXPLAIN rows
and returns index recommendations. Uses SchemaContextBuilder with
detail: :with_cardinality so the prompt includes information_schema.
STATISTICS cardinality per index.
Fifth and final extracted AI prompt builder. Extracts table names
from Rails migration helpers and raw SQL ALTER TABLE statements,
pulls their schema via SchemaContextBuilder, and sends to the
client.

Completes Stage B's AI extraction. Stage C rewrites the Rails
concern to delegate the 5 extracted actions to these classes.
Concern's anomaly_detection and root_cause stay in place because
they depend on the Redis-backed SlowQueryMonitor.
Returns the absolute path to the shared ERB template directory
that Stage D will populate. Adapters register this path with
their view loader.

The directory does not exist yet — Stage D will create it and
move the template files from app/views/mysql_genius/queries/
into it. This method just returns the well-formed string.
The action now builds an ActiveRecordAdapter and Columns service
instance, calls it with the query params, and maps the tagged
Result struct to HTTP status codes (:ok → 200, :blocked → 403,
:not_found → 404). Response bodies are unchanged.

The private masked_column?(name) helper added in the 0.4.1 hotfix
is deleted — Core::Analysis::Columns handles masked-column
filtering internally via Core::SqlValidator.masked_column?. The
regression spec from Stage A guards the "does not raise
NoMethodError" behavior at the HTTP layer.

A new private rails_connection helper builds the
ActiveRecordAdapter wrapper for reuse by subsequent Stage C tasks.
describe_query, schema_review, rewrite_query, index_advisor, and
migration_risk are now thin delegators mirroring the Phase 1b
pattern. Each action parses params, constructs the appropriate
Core::Ai::* builder, and returns its JSON result.

ai_domain_context private helper retained as a thin shim for
anomaly_detection and root_cause (which stay inline due to Redis
dependency); its Rails-FK string moves into the frozen
RAILS_DOMAIN_CONTEXT constant passed to Core::Ai::Config as
domain_context:. build_schema_for_query deleted — RewriteQuery
uses SchemaContextBuilder directly.

anomaly_detection and root_cause stay inline because they depend
on the Redis-backed SlowQueryMonitor. No scope change.
Concern module providing path_for(name) and render_partial(name)
as the 2-method contract the shared templates depend on. Both
are exposed as helper_method so they work from ERB.

QueriesController includes the concern. The engine registers
Core.views_path as an additional app/views path so Rails finds
the shared templates after they move in the next task.

No template changes yet; suite stays green because no template
actually calls path_for or render_partial.
Moves all 11 template files from app/views/mysql_genius/queries/
to gems/mysql_genius-core/lib/mysql_genius/core/views/mysql_genius/queries/.
Renames index.html.erb to dashboard.html.erb.

Rewrites every mysql_genius.*_path (18 URL helpers) to path_for(:*)
and every render "mysql_genius/queries/tab_*" (10 partial renders)
to render_partial(:*), calling through the SharedViewHelpers contract.
Rails::VERSION::MAJOR/MINOR references become @framework_version_major/minor
instance variables populated by the controller's index action.

QueriesController#index explicitly renders "mysql_genius/queries/dashboard"
so Rails finds it under the engine's registered views_path. The engine
registers Core.views_path before :add_view_paths (using before: ordering)
so ActionController's on_load block picks up both view roots. render_partial
delegates to view_context.render(partial:) to stay in ActionView's render
pipeline and avoid DoubleRenderError.
- mysql_genius-core: 0.4.1 -> 0.5.0
- mysql_genius: 0.4.1 -> 0.5.0
- mysql_genius.gemspec dep: "~> 0.4.0" -> "~> 0.5.0"
- CHANGELOG.md: new ## 0.5.0 section with Phase 2a additions
- gems/mysql_genius-core/CHANGELOG.md: new ## 0.5.0 section

The next commit tags v0.5.0 and kicks off the publish workflow,
which will push mysql_genius-core 0.5.0 first and then
mysql_genius 0.5.0 to rubygems.
@antarr antarr merged commit 38b7804 into main Apr 11, 2026
23 of 32 checks passed
@antarr antarr deleted the feature/mysql-genius-core-phase-2a branch April 11, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant