From aa4d67fcabc1b26aa9b921c5874db043ed443d49 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Fri, 27 Feb 2026 14:50:25 -0600 Subject: [PATCH 1/2] Stop auto-injecting engine migrations into host migrate paths Remove the append_migrations initializer so engine migrations don't run automatically with the host's db:migrate. Hosts should use the standard Rails workflow: rails coplan:install:migrations to copy engine migrations into db/migrate/, then rails db:migrate. Amp-Thread-ID: https://ampcode.com/threads/T-019ca0be-66f9-715f-8d94-944467f13298 Co-authored-by: Amp --- engine/lib/coplan/engine.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/engine/lib/coplan/engine.rb b/engine/lib/coplan/engine.rb index 8d89369..b62f16e 100644 --- a/engine/lib/coplan/engine.rb +++ b/engine/lib/coplan/engine.rb @@ -18,13 +18,6 @@ class Engine < ::Rails::Engine app.config.assets.paths << Engine.root.join("app/javascript") end - initializer "coplan.append_migrations", before: :load_config_initializers do |app| - config.paths["db/migrate"].expanded.each do |path| - app.config.paths["db/migrate"] << path - ActiveRecord::Migrator.migrations_paths << path - end - end - initializer "coplan.factories", after: "factory_bot.set_factory_paths" do if defined?(FactoryBot) FactoryBot.definition_file_paths << Engine.root.join("spec", "factories") From c935d5898bad785a375039f7b514af3ffa062698 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Mon, 2 Mar 2026 14:34:06 -0500 Subject: [PATCH 2/2] Auto-copy engine migrations before db:migrate Add a rake task that enhances db:migrate to run co_plan:install:migrations first, so hosts never silently fall behind on CoPlan schema changes. Copy the existing engine migration into the host app's db/migrate/ with its original timestamp. Addresses review feedback on PR #36. Amp-Thread-ID: https://ampcode.com/threads/T-019caf1c-e05e-773a-9c73-047ff1d5c0e8 Co-authored-by: Amp --- ...0226200000_create_coplan_schema.co_plan.rb | 173 ++++++++++++++++++ engine/lib/tasks/coplan.rake | 4 + 2 files changed, 177 insertions(+) create mode 100644 db/migrate/20260226200000_create_coplan_schema.co_plan.rb create mode 100644 engine/lib/tasks/coplan.rake diff --git a/db/migrate/20260226200000_create_coplan_schema.co_plan.rb b/db/migrate/20260226200000_create_coplan_schema.co_plan.rb new file mode 100644 index 0000000..3802d8d --- /dev/null +++ b/db/migrate/20260226200000_create_coplan_schema.co_plan.rb @@ -0,0 +1,173 @@ +class CreateCoplanSchema < ActiveRecord::Migration[8.1] + def change + create_table :coplan_users, id: { type: :string, limit: 36 } do |t| + t.string :external_id, null: false + t.string :email + t.string :name, null: false + t.boolean :admin, default: false, null: false + t.json :metadata + t.timestamps + end + + add_index :coplan_users, :external_id, unique: true + add_index :coplan_users, :email, unique: true + + create_table :coplan_plans, id: { type: :string, limit: 36 } do |t| + t.string :title, null: false + t.string :status, default: "brainstorm", null: false + t.integer :current_revision, default: 0, null: false + t.string :created_by_user_id, limit: 36, null: false + t.string :current_plan_version_id, limit: 36 + t.json :tags + t.json :metadata + t.timestamps + end + + add_index :coplan_plans, :status + add_index :coplan_plans, :updated_at + add_index :coplan_plans, :created_by_user_id + add_foreign_key :coplan_plans, :coplan_users, column: :created_by_user_id + + create_table :coplan_plan_versions, id: { type: :string, limit: 36 } do |t| + t.string :plan_id, limit: 36, null: false + t.integer :revision, null: false + t.text :content_markdown, null: false + t.string :content_sha256, null: false + t.text :diff_unified + t.text :change_summary + t.text :reason + t.text :prompt_excerpt + t.json :operations_json + t.integer :base_revision + t.string :actor_id, limit: 36 + t.string :actor_type, null: false + t.string :ai_provider + t.string :ai_model + t.timestamp :created_at, null: false + end + + add_index :coplan_plan_versions, :plan_id + add_index :coplan_plan_versions, [:plan_id, :revision], unique: true + add_index :coplan_plan_versions, [:plan_id, :created_at] + add_foreign_key :coplan_plan_versions, :coplan_plans, column: :plan_id + + # Now that coplan_plan_versions exists, add the FK for current_plan_version_id + add_foreign_key :coplan_plans, :coplan_plan_versions, column: :current_plan_version_id + + create_table :coplan_plan_collaborators, id: { type: :string, limit: 36 } do |t| + t.string :plan_id, limit: 36, null: false + t.string :user_id, limit: 36, null: false + t.string :added_by_user_id, limit: 36 + t.string :role, null: false + t.timestamps + end + + add_index :coplan_plan_collaborators, :plan_id + add_index :coplan_plan_collaborators, :user_id + add_index :coplan_plan_collaborators, :added_by_user_id + add_index :coplan_plan_collaborators, [:plan_id, :user_id], unique: true + add_foreign_key :coplan_plan_collaborators, :coplan_plans, column: :plan_id + add_foreign_key :coplan_plan_collaborators, :coplan_users, column: :user_id + add_foreign_key :coplan_plan_collaborators, :coplan_users, column: :added_by_user_id + + create_table :coplan_comment_threads, id: { type: :string, limit: 36 } do |t| + t.string :plan_id, limit: 36, null: false + t.string :plan_version_id, limit: 36, null: false + t.string :created_by_user_id, limit: 36, null: false + t.string :resolved_by_user_id, limit: 36 + t.string :addressed_in_plan_version_id, limit: 36 + t.string :out_of_date_since_version_id, limit: 36 + t.string :status, default: "open", null: false + t.boolean :out_of_date, default: false, null: false + t.text :anchor_text + t.text :anchor_context + t.integer :anchor_start + t.integer :anchor_end + t.integer :anchor_revision + t.integer :start_line + t.integer :end_line + t.timestamps + end + + add_index :coplan_comment_threads, [:plan_id, :status] + add_index :coplan_comment_threads, [:plan_id, :out_of_date] + add_foreign_key :coplan_comment_threads, :coplan_plans, column: :plan_id + add_foreign_key :coplan_comment_threads, :coplan_plan_versions, column: :plan_version_id + add_foreign_key :coplan_comment_threads, :coplan_plan_versions, column: :addressed_in_plan_version_id + add_foreign_key :coplan_comment_threads, :coplan_plan_versions, column: :out_of_date_since_version_id + add_foreign_key :coplan_comment_threads, :coplan_users, column: :created_by_user_id + add_foreign_key :coplan_comment_threads, :coplan_users, column: :resolved_by_user_id + + create_table :coplan_comments, id: { type: :string, limit: 36 } do |t| + t.string :comment_thread_id, limit: 36, null: false + t.string :author_id, limit: 36 + t.string :author_type, null: false + t.string :agent_name + t.text :body_markdown, null: false + t.timestamps + end + + add_index :coplan_comments, [:comment_thread_id, :created_at] + add_foreign_key :coplan_comments, :coplan_comment_threads, column: :comment_thread_id + + create_table :coplan_edit_leases, id: { type: :string, limit: 36 } do |t| + t.string :plan_id, limit: 36, null: false + t.string :holder_id, limit: 36 + t.string :holder_type, null: false + t.string :lease_token_digest, null: false + t.timestamp :expires_at, null: false + t.timestamp :last_heartbeat_at, null: false + t.timestamps + end + + add_index :coplan_edit_leases, :plan_id, unique: true + add_foreign_key :coplan_edit_leases, :coplan_plans, column: :plan_id + + create_table :coplan_edit_sessions, id: { type: :string, limit: 36 } do |t| + t.string :plan_id, limit: 36, null: false + t.string :plan_version_id, limit: 36 + t.string :actor_id, limit: 36 + t.string :actor_type, null: false + t.string :status, default: "open", null: false + t.integer :base_revision, null: false + t.text :draft_content, size: :long + t.text :change_summary + t.json :operations_json, null: false + t.timestamp :expires_at, null: false + t.timestamp :committed_at + t.timestamps + end + + add_index :coplan_edit_sessions, [:plan_id, :status] + add_foreign_key :coplan_edit_sessions, :coplan_plans, column: :plan_id + add_foreign_key :coplan_edit_sessions, :coplan_plan_versions, column: :plan_version_id + + create_table :coplan_api_tokens, id: { type: :string, limit: 36 } do |t| + t.string :user_id, limit: 36, null: false + t.string :name, null: false + t.string :token_digest, null: false + t.string :token_prefix, limit: 8 + t.timestamp :expires_at + t.timestamp :revoked_at + t.timestamp :last_used_at + t.timestamps + end + + add_index :coplan_api_tokens, :user_id + add_index :coplan_api_tokens, :token_digest, unique: true + add_foreign_key :coplan_api_tokens, :coplan_users, column: :user_id + + create_table :coplan_automated_plan_reviewers, id: { type: :string, limit: 36 } do |t| + t.string :key, null: false + t.string :name, null: false + t.text :prompt_text, null: false + t.string :ai_provider, default: "openai", null: false + t.string :ai_model, null: false + t.json :trigger_statuses, null: false + t.boolean :enabled, default: true, null: false + t.timestamps + end + + add_index :coplan_automated_plan_reviewers, :key, unique: true + end +end diff --git a/engine/lib/tasks/coplan.rake b/engine/lib/tasks/coplan.rake new file mode 100644 index 0000000..04afeb7 --- /dev/null +++ b/engine/lib/tasks/coplan.rake @@ -0,0 +1,4 @@ +# Automatically copy engine migrations before db:migrate so hosts +# never silently fall behind on schema changes. +# Rails derives the task name from CoPlan → co_plan. +Rake::Task["db:migrate"].enhance(["co_plan:install:migrations"]) if Rake::Task.task_defined?("db:migrate")