From a828a6e5d49ba9628d3c8bf6fac20d7c774c41d1 Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Fri, 21 Aug 2026 15:52:19 -0500 Subject: [PATCH 1/2] Autocorrect the accumulated RuboCop offenses RuboCop has never been enforced here, so 895 offenses had built up across 75 files. 882 of them were Layout/SpaceInsideArrayLiteralBrackets: the repo uses rubocop-rails-omakase, which wants `[ a, b ]`, and much of the codebase was written as `[a, b]`. All of it was flagged Safe Correctable, so this is `bin/rubocop -a` with nothing done by hand. Every offense is gone. Formatting only. Ignoring whitespace, the diff touches five files: three blank lines and one trailing comma removed, plus a RedundantReturn in CoPlan::WebPushSubscription#device_label, where the `return` was on the method's last expression (the early-return guard above it is untouched). None of it changes behavior. Suite is unchanged: 1658 examples, 0 failures before and after. Co-Authored-By: Claude Opus 5 --- app/admin/dashboard.rb | 1 + app/admin/folders.rb | 4 +- app/controllers/sessions_controller.rb | 2 +- app/jobs/slack_notification_job.rb | 2 +- config/initializers/active_admin.rb | 2 +- ...0220171817_create_active_admin_comments.rb | 2 +- ...0226200000_create_coplan_schema.co_plan.rb | 14 +- ...1037_create_coplan_plan_viewers.co_plan.rb | 2 +- ...858_create_coplan_notifications.co_plan.rb | 4 +- ...260403220000_create_coplan_tags.co_plan.rb | 2 +- ...182145_create_coplan_references.co_plan.rb | 4 +- ...11926_create_coplan_plan_events.co_plan.rb | 2 +- ...09_create_coplan_search_queries.co_plan.rb | 2 +- ...716154625_create_coplan_folders.co_plan.rb | 2 +- ...on_minting_to_coplan_api_tokens.co_plan.rb | 4 +- .../coplan/agent_instructions_controller.rb | 8 +- .../coplan/api/v1/operations_controller.rb | 6 +- .../coplan/api/v1/sessions_controller.rb | 2 +- .../controllers/coplan/comments_controller.rb | 4 +- .../app/controllers/coplan/llms_controller.rb | 2 +- .../coplan/notifications_controller.rb | 4 +- .../coplan/plan_versions_controller.rb | 4 +- .../controllers/coplan/plans_controller.rb | 2 +- .../controllers/coplan/users_controller.rb | 4 +- .../app/helpers/coplan/application_helper.rb | 2 +- engine/app/models/coplan/edit_session.rb | 2 +- engine/app/models/coplan/plan_tag.rb | 2 +- engine/app/models/coplan/user.rb | 1 - .../models/coplan/web_push_subscription.rb | 28 +-- .../services/coplan/plans/apply_operations.rb | 10 +- .../coplan/plans/diff_to_operations.rb | 8 +- .../coplan/plans/markdown_text_extractor.rb | 4 +- .../coplan/plans/position_resolver.rb | 24 +-- .../services/coplan/plans/transform_range.rb | 8 +- engine/config/routes.rb | 50 ++--- .../20260226200000_create_coplan_schema.rb | 14 +- ...60327000000_create_coplan_notifications.rb | 4 +- ...260327000000_create_coplan_plan_viewers.rb | 2 +- .../20260403000000_create_coplan_tags.rb | 2 +- ...20260410000000_create_coplan_references.rb | 4 +- ...0260519000000_create_coplan_plan_events.rb | 2 +- ...0601000001_create_coplan_search_queries.rb | 2 +- .../20260716000000_create_coplan_folders.rb | 2 +- engine/lib/coplan/engine.rb | 1 - spec/factories/edit_sessions.rb | 2 +- .../coplan/markdown_helper_checkbox_spec.rb | 2 +- spec/jobs/commit_expired_session_job_spec.rb | 6 +- spec/lib/coplan/analytics_spec.rb | 4 +- spec/lib/tasks/coplan_summaries_spec.rb | 2 +- spec/models/coplan/notification_spec.rb | 2 +- spec/models/coplan/reference_spec.rb | 4 +- spec/models/edit_session_spec.rb | 2 +- spec/models/notification_spec.rb | 4 +- spec/models/plan_collaborator_spec.rb | 6 +- spec/models/plan_spec.rb | 8 +- spec/models/plan_viewer_spec.rb | 8 +- spec/models/search_query_spec.rb | 2 +- spec/models/tag_spec.rb | 18 +- spec/requests/api/v1/operations_spec.rb | 10 +- spec/requests/api/v1/plan_types_spec.rb | 6 +- spec/requests/api/v1/plans_spec.rb | 20 +- spec/requests/api/v1/sessions_spec.rb | 48 ++--- spec/requests/plan_content_editing_spec.rb | 6 +- spec/requests/plan_events_spec.rb | 14 +- spec/requests/users_spec.rb | 2 +- .../coplan/comments/process_mentions_spec.rb | 8 +- .../plan_types/install_defaults_spec.rb | 2 +- spec/services/plans/apply_operations_spec.rb | 90 ++++---- spec/services/plans/commit_session_spec.rb | 48 ++--- .../services/plans/diff_to_operations_spec.rb | 46 ++-- spec/services/plans/position_resolver_spec.rb | 8 +- spec/services/plans/transform_range_spec.rb | 204 +++++++++--------- spec/support/analytics_helpers.rb | 2 +- spec/system/comment_ux_spec.rb | 2 +- spec/system/human_editing_spec.rb | 2 +- 75 files changed, 422 insertions(+), 423 deletions(-) diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb index b57f92fd..92158bc1 100644 --- a/app/admin/dashboard.rb +++ b/app/admin/dashboard.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ActiveAdmin.register_page "Dashboard" do menu priority: 1, label: proc { I18n.t("active_admin.dashboard") } diff --git a/app/admin/folders.rb b/app/admin/folders.rb index 01fde64f..e4f24881 100644 --- a/app/admin/folders.rb +++ b/app/admin/folders.rb @@ -49,8 +49,8 @@ f.input :name f.input :library_id, as: :select, collection: CoPlan::Library.includes(:owner).map { |l| [ "#{l.owner.try(:name) || l.owner_id} — #{l.name}", l.id ] } - f.input :parent, collection: CoPlan::Folder.order(:name).map { |folder| [folder.path, folder.id] } - f.input :created_by_user, collection: CoPlan::User.order(:name).map { |u| [u.name, u.id] } + f.input :parent, collection: CoPlan::Folder.order(:name).map { |folder| [ folder.path, folder.id ] } + f.input :created_by_user, collection: CoPlan::User.order(:name).map { |u| [ u.name, u.id ] } end f.actions end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1876cdc2..d469f68f 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,5 +1,5 @@ class SessionsController < ApplicationController - skip_before_action :authenticate_user!, only: [:new, :create] + skip_before_action :authenticate_user!, only: [ :new, :create ] def new end diff --git a/app/jobs/slack_notification_job.rb b/app/jobs/slack_notification_job.rb index 849fd474..63f1c62e 100644 --- a/app/jobs/slack_notification_job.rb +++ b/app/jobs/slack_notification_job.rb @@ -27,7 +27,7 @@ def compose_message(thread, plan) comment_body = first_comment_body(thread).truncate(300) plan_url = CoPlan::Engine.routes.url_helpers.plan_url(plan, **default_url_options) - lines = ["New comment on *#{plan.title}*:"] + lines = [ "New comment on *#{plan.title}*:" ] if thread.anchor_text.present? lines << "> _#{thread.anchor_text.truncate(120)}_" end diff --git a/config/initializers/active_admin.rb b/config/initializers/active_admin.rb index a2ccc017..35b4eb06 100644 --- a/config/initializers/active_admin.rb +++ b/config/initializers/active_admin.rb @@ -160,7 +160,7 @@ # You can exclude possibly sensitive model attributes from being displayed, # added to forms, or exported by default by ActiveAdmin # - config.filter_attributes = [:encrypted_password, :password, :password_confirmation] + config.filter_attributes = [ :encrypted_password, :password, :password_confirmation ] # == Localize Date/Time Format # diff --git a/db/migrate/20260220171817_create_active_admin_comments.rb b/db/migrate/20260220171817_create_active_admin_comments.rb index 70c21d92..f0c969b1 100644 --- a/db/migrate/20260220171817_create_active_admin_comments.rb +++ b/db/migrate/20260220171817_create_active_admin_comments.rb @@ -7,7 +7,7 @@ def self.up t.references :author, polymorphic: true t.timestamps end - add_index :active_admin_comments, [:namespace] + add_index :active_admin_comments, [ :namespace ] end def self.down diff --git a/db/migrate/20260226200000_create_coplan_schema.co_plan.rb b/db/migrate/20260226200000_create_coplan_schema.co_plan.rb index 5c07bf08..87a3738a 100644 --- a/db/migrate/20260226200000_create_coplan_schema.co_plan.rb +++ b/db/migrate/20260226200000_create_coplan_schema.co_plan.rb @@ -54,8 +54,8 @@ def change 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_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 @@ -72,7 +72,7 @@ def change 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_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 @@ -96,8 +96,8 @@ def change t.timestamps end - add_index :coplan_comment_threads, [:plan_id, :status] - add_index :coplan_comment_threads, [:plan_id, :out_of_date] + 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 @@ -114,7 +114,7 @@ def change t.timestamps end - add_index :coplan_comments, [:comment_thread_id, :created_at] + 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| @@ -145,7 +145,7 @@ def change t.timestamps end - add_index :coplan_edit_sessions, [:plan_id, :status] + 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 diff --git a/db/migrate/20260327151037_create_coplan_plan_viewers.co_plan.rb b/db/migrate/20260327151037_create_coplan_plan_viewers.co_plan.rb index 99187101..440e1f72 100644 --- a/db/migrate/20260327151037_create_coplan_plan_viewers.co_plan.rb +++ b/db/migrate/20260327151037_create_coplan_plan_viewers.co_plan.rb @@ -8,7 +8,7 @@ def change t.timestamps end - add_index :coplan_plan_viewers, [:plan_id, :user_id], unique: true + add_index :coplan_plan_viewers, [ :plan_id, :user_id ], unique: true add_index :coplan_plan_viewers, :last_seen_at add_foreign_key :coplan_plan_viewers, :coplan_plans, column: :plan_id add_foreign_key :coplan_plan_viewers, :coplan_users, column: :user_id diff --git a/db/migrate/20260327170858_create_coplan_notifications.co_plan.rb b/db/migrate/20260327170858_create_coplan_notifications.co_plan.rb index bb538bd9..58baf442 100644 --- a/db/migrate/20260327170858_create_coplan_notifications.co_plan.rb +++ b/db/migrate/20260327170858_create_coplan_notifications.co_plan.rb @@ -12,8 +12,8 @@ def change t.timestamps end - add_index :coplan_notifications, [:user_id, :read_at], name: "index_coplan_notifications_on_user_id_and_read_at" - add_index :coplan_notifications, [:comment_thread_id, :user_id], name: "index_coplan_notifications_on_thread_and_user" + add_index :coplan_notifications, [ :user_id, :read_at ], name: "index_coplan_notifications_on_user_id_and_read_at" + add_index :coplan_notifications, [ :comment_thread_id, :user_id ], name: "index_coplan_notifications_on_thread_and_user" add_index :coplan_notifications, :plan_id, name: "index_coplan_notifications_on_plan_id" add_foreign_key :coplan_notifications, :coplan_users, column: :user_id diff --git a/db/migrate/20260403220000_create_coplan_tags.co_plan.rb b/db/migrate/20260403220000_create_coplan_tags.co_plan.rb index 90b429f6..2bbe0ca2 100644 --- a/db/migrate/20260403220000_create_coplan_tags.co_plan.rb +++ b/db/migrate/20260403220000_create_coplan_tags.co_plan.rb @@ -15,7 +15,7 @@ def change t.timestamps end - add_index :coplan_plan_tags, [:plan_id, :tag_id], unique: true + add_index :coplan_plan_tags, [ :plan_id, :tag_id ], unique: true add_index :coplan_plan_tags, :tag_id add_foreign_key :coplan_plan_tags, :coplan_plans, column: :plan_id add_foreign_key :coplan_plan_tags, :coplan_tags, column: :tag_id diff --git a/db/migrate/20260410182145_create_coplan_references.co_plan.rb b/db/migrate/20260410182145_create_coplan_references.co_plan.rb index ff671c66..b59f47d4 100644 --- a/db/migrate/20260410182145_create_coplan_references.co_plan.rb +++ b/db/migrate/20260410182145_create_coplan_references.co_plan.rb @@ -12,8 +12,8 @@ def change t.timestamps end - add_index :coplan_references, [:plan_id, :key], unique: true - add_index :coplan_references, [:plan_id, :url], unique: true + add_index :coplan_references, [ :plan_id, :key ], unique: true + add_index :coplan_references, [ :plan_id, :url ], unique: true add_index :coplan_references, :target_plan_id add_index :coplan_references, :source add_foreign_key :coplan_references, :coplan_plans, column: :plan_id diff --git a/db/migrate/20260519011926_create_coplan_plan_events.co_plan.rb b/db/migrate/20260519011926_create_coplan_plan_events.co_plan.rb index e618da72..1aa72f1f 100644 --- a/db/migrate/20260519011926_create_coplan_plan_events.co_plan.rb +++ b/db/migrate/20260519011926_create_coplan_plan_events.co_plan.rb @@ -13,7 +13,7 @@ def change t.datetime :created_at, null: false t.index :plan_id - t.index [:plan_id, :created_at] + t.index [ :plan_id, :created_at ] t.index :event_type end end diff --git a/db/migrate/20260601202009_create_coplan_search_queries.co_plan.rb b/db/migrate/20260601202009_create_coplan_search_queries.co_plan.rb index d86ff2e1..e52f3944 100644 --- a/db/migrate/20260601202009_create_coplan_search_queries.co_plan.rb +++ b/db/migrate/20260601202009_create_coplan_search_queries.co_plan.rb @@ -10,7 +10,7 @@ def change t.timestamp :created_at, null: false end - add_index :coplan_search_queries, [:user_id, :created_at] + add_index :coplan_search_queries, [ :user_id, :created_at ] add_foreign_key :coplan_search_queries, :coplan_users, column: :user_id end end diff --git a/db/migrate/20260716154625_create_coplan_folders.co_plan.rb b/db/migrate/20260716154625_create_coplan_folders.co_plan.rb index ed34219d..9ff5c480 100644 --- a/db/migrate/20260716154625_create_coplan_folders.co_plan.rb +++ b/db/migrate/20260716154625_create_coplan_folders.co_plan.rb @@ -10,7 +10,7 @@ def change # Unique per sibling group. MySQL treats NULLs as distinct in unique # indexes, so root-level (parent_id IS NULL) uniqueness is enforced by # the model validation instead — same approach either way for app code. - t.index [:parent_id, :name], unique: true + t.index [ :parent_id, :name ], unique: true t.index :created_by_user_id end diff --git a/db/migrate/20260815170755_add_session_minting_to_coplan_api_tokens.co_plan.rb b/db/migrate/20260815170755_add_session_minting_to_coplan_api_tokens.co_plan.rb index 67115155..721bf938 100644 --- a/db/migrate/20260815170755_add_session_minting_to_coplan_api_tokens.co_plan.rb +++ b/db/migrate/20260815170755_add_session_minting_to_coplan_api_tokens.co_plan.rb @@ -11,8 +11,8 @@ def change unless column_exists?(:coplan_api_tokens, :parent_id) add_column :coplan_api_tokens, :parent_id, :string, limit: 36 end - unless index_exists?(:coplan_api_tokens, [:parent_id, :revoked_at]) - add_index :coplan_api_tokens, [:parent_id, :revoked_at] + unless index_exists?(:coplan_api_tokens, [ :parent_id, :revoked_at ]) + add_index :coplan_api_tokens, [ :parent_id, :revoked_at ] end unless foreign_key_exists?(:coplan_api_tokens, column: :parent_id) add_foreign_key :coplan_api_tokens, :coplan_api_tokens, column: :parent_id diff --git a/engine/app/controllers/coplan/agent_instructions_controller.rb b/engine/app/controllers/coplan/agent_instructions_controller.rb index 7a4efc6b..d4957649 100644 --- a/engine/app/controllers/coplan/agent_instructions_controller.rb +++ b/engine/app/controllers/coplan/agent_instructions_controller.rb @@ -43,10 +43,10 @@ def show CoPlan::Current.user = current_user @instructions_url = coplan.agent_instructions_url - @instructions_markdown = render_to_string(:show, formats: [:text], layout: false) - render :show, formats: [:html] + @instructions_markdown = render_to_string(:show, formats: [ :text ], layout: false) + render :show, formats: [ :html ] else - render layout: false, content_type: "text/markdown", formats: [:text] + render layout: false, content_type: "text/markdown", formats: [ :text ] end end @@ -58,7 +58,7 @@ def organizing @auth_instructions = CoPlan.configuration.agent_auth_instructions @curl = CoPlan.configuration.agent_curl_prefix @base = "#{request.base_url}#{root_path.chomp("/")}" - render layout: false, content_type: "text/markdown", formats: [:text] + render layout: false, content_type: "text/markdown", formats: [ :text ] end private diff --git a/engine/app/controllers/coplan/api/v1/operations_controller.rb b/engine/app/controllers/coplan/api/v1/operations_controller.rb index f5aeba7a..4c46e314 100644 --- a/engine/app/controllers/coplan/api/v1/operations_controller.rb +++ b/engine/app/controllers/coplan/api/v1/operations_controller.rb @@ -160,7 +160,7 @@ def rebase_and_resolve(operations, base_revision, current_content) # Advance the working base snapshot so the next op resolves # against the result of this one (sequential semantics). - apply_result = Plans::ApplyOperations.call(content: working_base, operations: [op]) + apply_result = Plans::ApplyOperations.call(content: working_base, operations: [ op ]) working_base = apply_result[:content] rebased_op = op.dup @@ -169,7 +169,7 @@ def rebase_and_resolve(operations, base_revision, current_content) # Advance verification content so the next op's conflict check # runs against the incrementally updated snapshot. - verify_step = Plans::ApplyOperations.call(content: verification_content, operations: [rebased_op]) + verify_step = Plans::ApplyOperations.call(content: verification_content, operations: [ rebased_op ]) verification_content = verify_step[:content] rescue Plans::TransformRange::Conflict => e render json: { @@ -292,7 +292,7 @@ def verify_transformed_ranges!(op, transformed_ranges, content) if include_heading # Verify the heading is the first line of the section range first_line_end = content.index("\n", tr[0]) || tr[1] - first_line = content[tr[0]...[first_line_end, tr[1]].min] + first_line = content[tr[0]...[ first_line_end, tr[1] ].min] unless first_line&.rstrip == op["heading"]&.rstrip render json: { error: "Conflict: section at target position has changed", diff --git a/engine/app/controllers/coplan/api/v1/sessions_controller.rb b/engine/app/controllers/coplan/api/v1/sessions_controller.rb index 57bf616a..415a5ca6 100644 --- a/engine/app/controllers/coplan/api/v1/sessions_controller.rb +++ b/engine/app/controllers/coplan/api/v1/sessions_controller.rb @@ -4,7 +4,7 @@ module V1 class SessionsController < BaseController before_action :set_plan before_action :authorize_plan_access! - before_action :set_session, only: [:show, :commit] + before_action :set_session, only: [ :show, :commit ] # POST /api/v1/plans/:plan_id/sessions # Cloud personas create sessions via direct Ruby service calls, not this endpoint. diff --git a/engine/app/controllers/coplan/comments_controller.rb b/engine/app/controllers/coplan/comments_controller.rb index 8c289351..e516efb9 100644 --- a/engine/app/controllers/coplan/comments_controller.rb +++ b/engine/app/controllers/coplan/comments_controller.rb @@ -34,7 +34,7 @@ def create # broadcast echoes back. respond_to do |format| format.turbo_stream do - html = render_to_string(partial: "coplan/comments/comment", locals: locals, formats: [:html]) + html = render_to_string(partial: "coplan/comments/comment", locals: locals, formats: [ :html ]) render turbo_stream: turbo_stream.append(target, html) end format.html { redirect_to plan_path(@plan), notice: "Reply added." } @@ -59,7 +59,7 @@ def destroy locals = { comment: comment } # Requestless render for the broadcast; request-scoped only inline. Broadcaster.replace_to(@plan, target: target, partial: "coplan/comments/comment", locals: locals) - html = render_to_string(partial: "coplan/comments/comment", locals: locals, formats: [:html]) + html = render_to_string(partial: "coplan/comments/comment", locals: locals, formats: [ :html ]) inline_stream = turbo_stream.replace(target, html) end diff --git a/engine/app/controllers/coplan/llms_controller.rb b/engine/app/controllers/coplan/llms_controller.rb index 7496cf1a..ccf96bc7 100644 --- a/engine/app/controllers/coplan/llms_controller.rb +++ b/engine/app/controllers/coplan/llms_controller.rb @@ -3,7 +3,7 @@ class LlmsController < ApplicationController skip_before_action :authenticate_coplan_user! def show - render layout: false, content_type: "text/markdown", formats: [:text] + render layout: false, content_type: "text/markdown", formats: [ :text ] end end end diff --git a/engine/app/controllers/coplan/notifications_controller.rb b/engine/app/controllers/coplan/notifications_controller.rb index 73620113..e63f46d4 100644 --- a/engine/app/controllers/coplan/notifications_controller.rb +++ b/engine/app/controllers/coplan/notifications_controller.rb @@ -7,7 +7,7 @@ class NotificationsController < ApplicationController def index @filter = params[:filter] == "all" ? "all" : "unread" @notifications = current_user.notifications - .includes(:plan, :comment, comment_thread: [:created_by_user]) + .includes(:plan, :comment, comment_thread: [ :created_by_user ]) .newest_first .limit(INDEX_LIMIT) @@ -53,7 +53,7 @@ def mark_all_read respond_to do |format| format.turbo_stream { @notifications = current_user.notifications - .includes(:plan, :comment, comment_thread: [:created_by_user]) + .includes(:plan, :comment, comment_thread: [ :created_by_user ]) .newest_first .unread @unread_count = 0 diff --git a/engine/app/controllers/coplan/plan_versions_controller.rb b/engine/app/controllers/coplan/plan_versions_controller.rb index 32d052e6..819e1f9c 100644 --- a/engine/app/controllers/coplan/plan_versions_controller.rb +++ b/engine/app/controllers/coplan/plan_versions_controller.rb @@ -1,8 +1,8 @@ module CoPlan class PlanVersionsController < ApplicationController before_action :set_plan - before_action :set_version, only: [:show, :diff] - before_action :set_diff, only: [:show, :diff] + before_action :set_version, only: [ :show, :diff ] + before_action :set_diff, only: [ :show, :diff ] def show authorize!(@plan, :show?) diff --git a/engine/app/controllers/coplan/plans_controller.rb b/engine/app/controllers/coplan/plans_controller.rb index a9c2a657..37981b89 100644 --- a/engine/app/controllers/coplan/plans_controller.rb +++ b/engine/app/controllers/coplan/plans_controller.rb @@ -1,6 +1,6 @@ module CoPlan class PlansController < ApplicationController - before_action :set_plan, only: [:show, :edit, :update, :publish, :hide, :archive, :unarchive, :move_to_folder, :toggle_checkbox, :history, :edit_content, :update_content, :preview] + before_action :set_plan, only: [ :show, :edit, :update, :publish, :hide, :archive, :unarchive, :move_to_folder, :toggle_checkbox, :history, :edit_content, :update_content, :preview ] PER_PAGE = 20 diff --git a/engine/app/controllers/coplan/users_controller.rb b/engine/app/controllers/coplan/users_controller.rb index 5003667c..26cb70c4 100644 --- a/engine/app/controllers/coplan/users_controller.rb +++ b/engine/app/controllers/coplan/users_controller.rb @@ -33,9 +33,9 @@ def search def user_json(user) if user.respond_to?(:id) - ALLOWED_FIELDS.to_h { |f| [f, user.public_send(f)] } + ALLOWED_FIELDS.to_h { |f| [ f, user.public_send(f) ] } else - ALLOWED_FIELDS.to_h { |f| [f, user[f]] } + ALLOWED_FIELDS.to_h { |f| [ f, user[f] ] } end end end diff --git a/engine/app/helpers/coplan/application_helper.rb b/engine/app/helpers/coplan/application_helper.rb index 6553691a..d53b20ff 100644 --- a/engine/app/helpers/coplan/application_helper.rb +++ b/engine/app/helpers/coplan/application_helper.rb @@ -4,7 +4,7 @@ module ApplicationHelper FAVICON_COLORS = { "production" => { start: "#3B82F6", stop: "#1E40AF" }, "staging" => { start: "#F59E0B", stop: "#D97706" }, - "development" => { start: "#10B981", stop: "#047857" }, + "development" => { start: "#10B981", stop: "#047857" } }.freeze def coplan_favicon_tag diff --git a/engine/app/models/coplan/edit_session.rb b/engine/app/models/coplan/edit_session.rb index 8c8eb061..3b73eb25 100644 --- a/engine/app/models/coplan/edit_session.rb +++ b/engine/app/models/coplan/edit_session.rb @@ -40,7 +40,7 @@ def has_operations? end def add_operation(op) - self.operations_json = operations_json + [op] + self.operations_json = operations_json + [ op ] save! end diff --git a/engine/app/models/coplan/plan_tag.rb b/engine/app/models/coplan/plan_tag.rb index 58140915..174f34e9 100644 --- a/engine/app/models/coplan/plan_tag.rb +++ b/engine/app/models/coplan/plan_tag.rb @@ -7,7 +7,7 @@ class PlanTag < ApplicationRecord # Plan-level after_save_commit doesn't fire when tags change (the Plan row # itself isn't dirty), so re-denormalize the parent's `search_text` here. - after_commit :refresh_plan_search_text, on: [:create, :destroy] + after_commit :refresh_plan_search_text, on: [ :create, :destroy ] def self.ransackable_attributes(auth_object = nil) %w[id plan_id tag_id created_at updated_at] diff --git a/engine/app/models/coplan/user.rb b/engine/app/models/coplan/user.rb index 101f3b5d..1577ac46 100644 --- a/engine/app/models/coplan/user.rb +++ b/engine/app/models/coplan/user.rb @@ -73,6 +73,5 @@ def voice_hotkey=(value) self.metadata ||= {} self.metadata["voice_hotkey"] = value end - end end diff --git a/engine/app/models/coplan/web_push_subscription.rb b/engine/app/models/coplan/web_push_subscription.rb index 94e6d88b..6a13ff47 100644 --- a/engine/app/models/coplan/web_push_subscription.rb +++ b/engine/app/models/coplan/web_push_subscription.rb @@ -55,23 +55,23 @@ def device_label return "Unknown browser" if ua.blank? browser = case ua - when /Edg\// then "Edge" - when /OPR\// then "Opera" - when /Firefox\// then "Firefox" - when /Chrome\// then "Chrome" - when /Safari\// then "Safari" - end + when /Edg\// then "Edge" + when /OPR\// then "Opera" + when /Firefox\// then "Firefox" + when /Chrome\// then "Chrome" + when /Safari\// then "Safari" + end os = case ua - when /iPhone OS|iOS/ then "iOS" - when /iPad/ then "iPadOS" - when /Android/ then "Android" - when /Mac OS X|Macintosh/ then "macOS" - when /Windows NT/ then "Windows" - when /Linux/ then "Linux" - end + when /iPhone OS|iOS/ then "iOS" + when /iPad/ then "iPadOS" + when /Android/ then "Android" + when /Mac OS X|Macintosh/ then "macOS" + when /Windows NT/ then "Windows" + when /Linux/ then "Linux" + end - return [browser, os].compact.join(" on ").presence || ua.truncate(80) + [ browser, os ].compact.join(" on ").presence || ua.truncate(80) end private diff --git a/engine/app/services/coplan/plans/apply_operations.rb b/engine/app/services/coplan/plans/apply_operations.rb index d0e19963..de1b06cc 100644 --- a/engine/app/services/coplan/plans/apply_operations.rb +++ b/engine/app/services/coplan/plans/apply_operations.rb @@ -78,7 +78,7 @@ def apply_replace_exact(op, index) delta = new_text.length - (range[1] - range[0]) replacements << { "resolved_range" => range, - "new_range" => [range[0], range[0] + new_text.length], + "new_range" => [ range[0], range[0] + new_text.length ], "delta" => delta } cumulative_delta += delta @@ -95,7 +95,7 @@ def apply_replace_exact(op, index) # `old_text` supplied by the caller. delta = new_text.length - (range[1] - range[0]) applied_data["resolved_range"] = range - applied_data["new_range"] = [range[0], range[0] + new_text.length] + applied_data["new_range"] = [ range[0], range[0] + new_text.length ] applied_data["delta"] = delta end @@ -119,7 +119,7 @@ def apply_insert_under_heading(op, index) applied_data = op.except("_pre_resolved_ranges") applied_data["resolved_range"] = insert_point - applied_data["new_range"] = [insert_point[0], insert_point[0] + content_to_insert.length] + applied_data["new_range"] = [ insert_point[0], insert_point[0] + content_to_insert.length ] applied_data["delta"] = content_to_insert.length applied_data end @@ -160,7 +160,7 @@ def apply_replace_section(op, index) delta = effective_content.length - (range[1] - range[0]) applied_data = op.except("_pre_resolved_ranges") applied_data["resolved_range"] = range - applied_data["new_range"] = [range[0], range[0] + effective_content.length] + applied_data["new_range"] = [ range[0], range[0] + effective_content.length ] applied_data["delta"] = delta applied_data end @@ -181,7 +181,7 @@ def apply_delete_paragraph_containing(op, index) applied_data = op.except("_pre_resolved_ranges") applied_data["resolved_range"] = range - applied_data["new_range"] = [range[0], range[0]] + applied_data["new_range"] = [ range[0], range[0] ] applied_data["delta"] = -deleted_length applied_data end diff --git a/engine/app/services/coplan/plans/diff_to_operations.rb b/engine/app/services/coplan/plans/diff_to_operations.rb index 9b246178..66581045 100644 --- a/engine/app/services/coplan/plans/diff_to_operations.rb +++ b/engine/app/services/coplan/plans/diff_to_operations.rb @@ -67,7 +67,7 @@ def call "op" => "replace_exact", "old_text" => old_text, "new_text" => new_text, - "_pre_resolved_ranges" => [[adjusted_start, adjusted_end]] + "_pre_resolved_ranges" => [ [ adjusted_start, adjusted_end ] ] } cumulative_delta += new_text.length - old_text.length @@ -78,7 +78,7 @@ def call private def build_line_offsets(lines) - offsets = [0] + offsets = [ 0 ] running = 0 lines.each do |line| running += line.length @@ -137,10 +137,10 @@ def group_hunks(sdiff) # pure insertion/deletion (empty line list), uses the anchor as a # zero-width range at offsets[anchor]. def char_range(line_indexes, anchor, offsets) - return [offsets[anchor], offsets[anchor]] if line_indexes.empty? + return [ offsets[anchor], offsets[anchor] ] if line_indexes.empty? first = line_indexes.first last = line_indexes.last - [offsets[first], offsets[last + 1]] + [ offsets[first], offsets[last + 1] ] end end end diff --git a/engine/app/services/coplan/plans/markdown_text_extractor.rb b/engine/app/services/coplan/plans/markdown_text_extractor.rb index 2e04e791..dea4fc1c 100644 --- a/engine/app/services/coplan/plans/markdown_text_extractor.rb +++ b/engine/app/services/coplan/plans/markdown_text_extractor.rb @@ -29,7 +29,7 @@ def call extract_text_nodes(doc, line_byte_offsets, byte_to_char, stripped, pos_map) - [stripped, pos_map] + [ stripped, pos_map ] end private @@ -50,7 +50,7 @@ def build_byte_to_char_map # Builds an array mapping 1-based line numbers to byte offsets. # line_byte_offsets[line_number] = byte offset of the first byte on that line. def build_line_byte_offsets - offsets = [nil, 0] # index 0 unused; line 1 starts at byte 0 + offsets = [ nil, 0 ] # index 0 unused; line 1 starts at byte 0 byte_offset = 0 @content.each_char do |char| byte_offset += char.bytesize diff --git a/engine/app/services/coplan/plans/position_resolver.rb b/engine/app/services/coplan/plans/position_resolver.rb index 72caf829..4fc3a392 100644 --- a/engine/app/services/coplan/plans/position_resolver.rb +++ b/engine/app/services/coplan/plans/position_resolver.rb @@ -59,7 +59,7 @@ def resolve_replace_exact raise OperationError, "replace_exact: occurrence #{occurrence} requested but only #{ranges.length} found" end - Resolution.new(op: "replace_exact", ranges: [ranges[occurrence - 1]]) + Resolution.new(op: "replace_exact", ranges: [ ranges[occurrence - 1] ]) end end end @@ -73,7 +73,7 @@ def resolve_insert_under_heading matches = [] @content.scan(pattern) do match_end = Regexp.last_match.end(0) - matches << [match_end, match_end] + matches << [ match_end, match_end ] end if matches.empty? @@ -103,7 +103,7 @@ def resolve_delete_paragraph_containing end para = matching.first - ranges = [deletion_range_for(para, paragraphs)] + ranges = [ deletion_range_for(para, paragraphs) ] Resolution.new(op: "delete_paragraph_containing", ranges: ranges) end @@ -112,7 +112,7 @@ def find_all_occurrences(text) ranges = [] start_pos = 0 while (idx = @content.index(text, start_pos)) - ranges << [idx, idx + text.length] + ranges << [ idx, idx + text.length ] start_pos = idx + text.length end ranges @@ -195,7 +195,7 @@ def resolve_replace_section end range = if include_heading - [section_start, section_end] + [ section_start, section_end ] else # Skip past the heading line itself heading_line_end = @content.index("\n", section_start) @@ -208,14 +208,14 @@ def resolve_replace_section # When trailing newlines are stripped, section_end can retreat # behind body_start. Use an empty range at body_start to avoid # an inverted range and keep the insertion point after the heading newline. - [body_start, [body_start, section_end].max] + [ body_start, [ body_start, section_end ].max ] else # Heading is the only line — body is empty - [section_end, section_end] + [ section_end, section_end ] end end - Resolution.new(op: "replace_section", ranges: [range]) + Resolution.new(op: "replace_section", ranges: [ range ]) end # Parse markdown headings, respecting code fences (``` blocks). @@ -274,16 +274,16 @@ def deletion_range_for(para, all_paragraphs) if all_paragraphs.length == 1 # Only paragraph — delete everything - [0, @content.length] + [ 0, @content.length ] elsif is_first # First paragraph: delete from text_start through the separator after it, # so the next paragraph becomes the start. - [para[:text_start], para[:sep_end]] + [ para[:text_start], para[:sep_end] ] elsif is_last # Last paragraph: delete from the separator before it (end of previous # paragraph's text) to the end of this paragraph's text. prev = all_paragraphs[idx - 1] - [prev[:text_end], para[:text_end]] + [ prev[:text_end], para[:text_end] ] else # Middle paragraph: delete from end of previous paragraph's text # through the separator after this paragraph, but keep one separator @@ -294,7 +294,7 @@ def deletion_range_for(para, all_paragraphs) # sep_end. That removes the paragraph and its trailing separator, and # the separator before it (from previous text_end to this text_start) # becomes the separator between prev and next. - [para[:text_start], para[:sep_end]] + [ para[:text_start], para[:sep_end] ] end end end diff --git a/engine/app/services/coplan/plans/transform_range.rb b/engine/app/services/coplan/plans/transform_range.rb index c4672f26..c945a87c 100644 --- a/engine/app/services/coplan/plans/transform_range.rb +++ b/engine/app/services/coplan/plans/transform_range.rb @@ -39,9 +39,9 @@ def self.transform(range, edit_data) if s == e # Insert point: shift if edit is strictly before if e2 <= s - return [s + delta, e + delta] + return [ s + delta, e + delta ] elsif s2 > s - return [s, e] + return [ s, e ] else raise Conflict, "Edit overlaps with insert point" end @@ -49,12 +49,12 @@ def self.transform(range, edit_data) # Case 1: Edit is entirely before our range (e2 <= s) if e2 <= s - return [s + delta, e + delta] + return [ s + delta, e + delta ] end # Case 2: Edit is entirely after our range (s2 >= e) if s2 >= e - return [s, e] + return [ s, e ] end # Case 3: Overlap — conflict diff --git a/engine/config/routes.rb b/engine/config/routes.rb index 237a8218..35cb7710 100644 --- a/engine/config/routes.rb +++ b/engine/config/routes.rb @@ -1,5 +1,5 @@ CoPlan::Engine.routes.draw do - resources :plans, only: [:index, :show, :edit, :update] do + resources :plans, only: [ :index, :show, :edit, :update ] do patch :publish, on: :member patch :hide, on: :member patch :archive, on: :member @@ -10,28 +10,28 @@ get :edit_content, on: :member patch :update_content, on: :member post :preview, on: :member - resources :versions, controller: "plan_versions", only: [:show] do + resources :versions, controller: "plan_versions", only: [ :show ] do get :diff, on: :member end - resources :references, controller: "references", only: [:create, :destroy] - resources :attachments, controller: "attachments", only: [:create, :destroy] + resources :references, controller: "references", only: [ :create, :destroy ] + resources :attachments, controller: "attachments", only: [ :create, :destroy ] # Cleans up a spoken remark and works out which passage it was about; # see DictationsController. - resources :dictations, only: [:create] - resources :comment_threads, only: [:create] do + resources :dictations, only: [ :create ] + resources :comment_threads, only: [ :create ] do member do patch :resolve patch :accept patch :discard patch :reopen end - resources :comments, only: [:create, :destroy] + resources :comments, only: [ :create, :destroy ] end end namespace :settings do root "settings#index" - resources :tokens, only: [:index, :create, :destroy] + resources :tokens, only: [ :index, :create, :destroy ] patch "theme", to: "settings#update_theme" patch "voice_hotkey", to: "settings#update_voice_hotkey" end @@ -39,11 +39,11 @@ # Web folder creation (sidebar "New folder" input) and reparenting (drag # a folder onto a folder). Rename/delete go through the API or admin for # now. - resources :folders, only: [:create, :update] + resources :folders, only: [ :create, :update ] # Read-only library browsing (folder-jump discovery). "library" without # an id is the signed-in user's own — handy for nav links. - resources :libraries, only: [:show] + resources :libraries, only: [ :show ] get "library", to: "libraries#mine", as: :my_library # Profile pages — the front door to a person's library. :id is a @@ -53,16 +53,16 @@ namespace :api do namespace :v1 do - resources :tags, only: [:index] + resources :tags, only: [ :index ] # Plan-type catalog (with templates) — agents read this before # creating a plan; see the Create Plan section of /agent-instructions. - resources :plan_types, only: [:index] - resources :folders, only: [:index, :create, :update, :destroy] + resources :plan_types, only: [ :index ] + resources :folders, only: [ :index, :create, :update, :destroy ] # The agent organization API: overview (show), bulk read (contents), # bulk write (organize), audit log (events). The bare /library routes # are the caller's own library, no id needed. - resources :libraries, only: [:index, :show] do + resources :libraries, only: [ :index, :show ] do member do get :contents get :events @@ -74,18 +74,18 @@ get "library/events", to: "libraries#events", as: :own_library_events post "library/organize", to: "libraries#organize", as: :own_library_organize - resources :plans, only: [:index, :show, :create, :update] do + resources :plans, only: [ :index, :show, :create, :update ] do get :versions, on: :member get :locations, on: :member get :comments, on: :member get :snapshot, on: :member - resource :content, only: [:update], controller: "content" - resource :lease, only: [:create, :update, :destroy], controller: "leases" - resources :operations, only: [:create] - resources :sessions, only: [:create, :show], controller: "sessions" do + resource :content, only: [ :update ], controller: "content" + resource :lease, only: [ :create, :update, :destroy ], controller: "leases" + resources :operations, only: [ :create ] + resources :sessions, only: [ :create, :show ], controller: "sessions" do post :commit, on: :member end - resources :comments, only: [:create], controller: "comments" do + resources :comments, only: [ :create ], controller: "comments" do post :reply, on: :member patch :resolve, on: :member patch :discard, on: :member @@ -93,8 +93,8 @@ # Deletes an individual comment (by comment ID, not thread ID). # Distinct from the routes above, which key off thread ID. delete "comments/:id/delete", to: "comments#destroy", as: :destroy_comment - resources :references, only: [:index, :create, :destroy] - resources :attachments, only: [:index, :create, :destroy] + resources :references, only: [ :index, :create, :destroy ] + resources :attachments, only: [ :index, :create, :destroy ] end resources :references, only: [] do get :search, on: :collection @@ -104,7 +104,7 @@ # the host's request auth alone, since it is how an agent gets the # Bearer token every other call requires. DELETE revokes whichever # token authenticated the request. - resources :tokens, only: [:create] + resources :tokens, only: [ :create ] delete "tokens/current", to: "tokens#destroy", as: :revoke_current_token end end @@ -113,7 +113,7 @@ get :search, on: :collection end - resources :notifications, only: [:index, :show] do + resources :notifications, only: [ :index, :show ] do member do patch :mark_read end @@ -137,7 +137,7 @@ # Web Push subscription management. Endpoint URLs come from the browser's # PushManager and uniquely identify a (browser, device, app) tuple per user. scope :web_push, module: "web_push", as: :web_push do - resource :subscription, only: [:create, :destroy], controller: "subscriptions" + resource :subscription, only: [ :create, :destroy ], controller: "subscriptions" # Turbo-frame target for the per-device list on the Settings page. # Reloaded by the settings Stimulus controller after enable/disable so # the list reflects the new browser without a full page refresh. diff --git a/engine/db/migrate/20260226200000_create_coplan_schema.rb b/engine/db/migrate/20260226200000_create_coplan_schema.rb index 5c07bf08..87a3738a 100644 --- a/engine/db/migrate/20260226200000_create_coplan_schema.rb +++ b/engine/db/migrate/20260226200000_create_coplan_schema.rb @@ -54,8 +54,8 @@ def change 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_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 @@ -72,7 +72,7 @@ def change 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_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 @@ -96,8 +96,8 @@ def change t.timestamps end - add_index :coplan_comment_threads, [:plan_id, :status] - add_index :coplan_comment_threads, [:plan_id, :out_of_date] + 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 @@ -114,7 +114,7 @@ def change t.timestamps end - add_index :coplan_comments, [:comment_thread_id, :created_at] + 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| @@ -145,7 +145,7 @@ def change t.timestamps end - add_index :coplan_edit_sessions, [:plan_id, :status] + 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 diff --git a/engine/db/migrate/20260327000000_create_coplan_notifications.rb b/engine/db/migrate/20260327000000_create_coplan_notifications.rb index 982f4e36..d41133a8 100644 --- a/engine/db/migrate/20260327000000_create_coplan_notifications.rb +++ b/engine/db/migrate/20260327000000_create_coplan_notifications.rb @@ -11,8 +11,8 @@ def change t.timestamps end - add_index :coplan_notifications, [:user_id, :read_at], name: "index_coplan_notifications_on_user_id_and_read_at" - add_index :coplan_notifications, [:comment_thread_id, :user_id], name: "index_coplan_notifications_on_thread_and_user" + add_index :coplan_notifications, [ :user_id, :read_at ], name: "index_coplan_notifications_on_user_id_and_read_at" + add_index :coplan_notifications, [ :comment_thread_id, :user_id ], name: "index_coplan_notifications_on_thread_and_user" add_index :coplan_notifications, :plan_id, name: "index_coplan_notifications_on_plan_id" add_foreign_key :coplan_notifications, :coplan_users, column: :user_id diff --git a/engine/db/migrate/20260327000000_create_coplan_plan_viewers.rb b/engine/db/migrate/20260327000000_create_coplan_plan_viewers.rb index 8a1daeab..2a8b9f0b 100644 --- a/engine/db/migrate/20260327000000_create_coplan_plan_viewers.rb +++ b/engine/db/migrate/20260327000000_create_coplan_plan_viewers.rb @@ -7,7 +7,7 @@ def change t.timestamps end - add_index :coplan_plan_viewers, [:plan_id, :user_id], unique: true + add_index :coplan_plan_viewers, [ :plan_id, :user_id ], unique: true add_index :coplan_plan_viewers, :last_seen_at add_foreign_key :coplan_plan_viewers, :coplan_plans, column: :plan_id add_foreign_key :coplan_plan_viewers, :coplan_users, column: :user_id diff --git a/engine/db/migrate/20260403000000_create_coplan_tags.rb b/engine/db/migrate/20260403000000_create_coplan_tags.rb index 1166007b..c9094eb6 100644 --- a/engine/db/migrate/20260403000000_create_coplan_tags.rb +++ b/engine/db/migrate/20260403000000_create_coplan_tags.rb @@ -14,7 +14,7 @@ def change t.timestamps end - add_index :coplan_plan_tags, [:plan_id, :tag_id], unique: true + add_index :coplan_plan_tags, [ :plan_id, :tag_id ], unique: true add_index :coplan_plan_tags, :tag_id add_foreign_key :coplan_plan_tags, :coplan_plans, column: :plan_id add_foreign_key :coplan_plan_tags, :coplan_tags, column: :tag_id diff --git a/engine/db/migrate/20260410000000_create_coplan_references.rb b/engine/db/migrate/20260410000000_create_coplan_references.rb index 6497a359..4769aa06 100644 --- a/engine/db/migrate/20260410000000_create_coplan_references.rb +++ b/engine/db/migrate/20260410000000_create_coplan_references.rb @@ -11,8 +11,8 @@ def change t.timestamps end - add_index :coplan_references, [:plan_id, :key], unique: true - add_index :coplan_references, [:plan_id, :url], unique: true + add_index :coplan_references, [ :plan_id, :key ], unique: true + add_index :coplan_references, [ :plan_id, :url ], unique: true add_index :coplan_references, :target_plan_id add_index :coplan_references, :source add_foreign_key :coplan_references, :coplan_plans, column: :plan_id diff --git a/engine/db/migrate/20260519000000_create_coplan_plan_events.rb b/engine/db/migrate/20260519000000_create_coplan_plan_events.rb index e7e65127..01f44367 100644 --- a/engine/db/migrate/20260519000000_create_coplan_plan_events.rb +++ b/engine/db/migrate/20260519000000_create_coplan_plan_events.rb @@ -12,7 +12,7 @@ def change t.datetime :created_at, null: false t.index :plan_id - t.index [:plan_id, :created_at] + t.index [ :plan_id, :created_at ] t.index :event_type end end diff --git a/engine/db/migrate/20260601000001_create_coplan_search_queries.rb b/engine/db/migrate/20260601000001_create_coplan_search_queries.rb index 8297dd84..49ccfd32 100644 --- a/engine/db/migrate/20260601000001_create_coplan_search_queries.rb +++ b/engine/db/migrate/20260601000001_create_coplan_search_queries.rb @@ -9,7 +9,7 @@ def change t.timestamp :created_at, null: false end - add_index :coplan_search_queries, [:user_id, :created_at] + add_index :coplan_search_queries, [ :user_id, :created_at ] add_foreign_key :coplan_search_queries, :coplan_users, column: :user_id end end diff --git a/engine/db/migrate/20260716000000_create_coplan_folders.rb b/engine/db/migrate/20260716000000_create_coplan_folders.rb index d896cffe..866ce35d 100644 --- a/engine/db/migrate/20260716000000_create_coplan_folders.rb +++ b/engine/db/migrate/20260716000000_create_coplan_folders.rb @@ -9,7 +9,7 @@ def change # Unique per sibling group. MySQL treats NULLs as distinct in unique # indexes, so root-level (parent_id IS NULL) uniqueness is enforced by # the model validation instead — same approach either way for app code. - t.index [:parent_id, :name], unique: true + t.index [ :parent_id, :name ], unique: true t.index :created_by_user_id end diff --git a/engine/lib/coplan/engine.rb b/engine/lib/coplan/engine.rb index 93e6f5a9..3cdce4d2 100644 --- a/engine/lib/coplan/engine.rb +++ b/engine/lib/coplan/engine.rb @@ -24,7 +24,6 @@ class Engine < ::Rails::Engine FactoryBot.definition_file_paths << Engine.root.join("spec", "factories") end end - end # Override table name prefix: isolate_namespace generates "co_plan_" diff --git a/spec/factories/edit_sessions.rb b/spec/factories/edit_sessions.rb index de456fb4..6d0bd0c9 100644 --- a/spec/factories/edit_sessions.rb +++ b/spec/factories/edit_sessions.rb @@ -14,7 +14,7 @@ end trait :with_operations do - operations_json { [{ "op" => "replace_exact", "old_text" => "old", "new_text" => "new" }] } + operations_json { [ { "op" => "replace_exact", "old_text" => "old", "new_text" => "new" } ] } end end end diff --git a/spec/helpers/coplan/markdown_helper_checkbox_spec.rb b/spec/helpers/coplan/markdown_helper_checkbox_spec.rb index e0429d5a..a066a1bb 100644 --- a/spec/helpers/coplan/markdown_helper_checkbox_spec.rb +++ b/spec/helpers/coplan/markdown_helper_checkbox_spec.rb @@ -136,7 +136,7 @@ def checkboxes_for(md) it "leaves ordered-list tasks non-interactive (toggle endpoint rejects them)" do checkboxes = checkboxes_for("1. [ ] Ordered task\n\n- [ ] Bullet task") interactive = checkboxes.reject { |cb| cb["disabled"] } - expect(interactive.map { |cb| cb["data-line-text"] }).to eq(["- [ ] Bullet task"]) + expect(interactive.map { |cb| cb["data-line-text"] }).to eq([ "- [ ] Bullet task" ]) end it "strips data-sourcepos from the rendered output" do diff --git a/spec/jobs/commit_expired_session_job_spec.rb b/spec/jobs/commit_expired_session_job_spec.rb index 9ab1c4ab..fc734f32 100644 --- a/spec/jobs/commit_expired_session_job_spec.rb +++ b/spec/jobs/commit_expired_session_job_spec.rb @@ -18,7 +18,7 @@ session = CoPlan::EditSession.create!( plan: plan, actor_type: "local_agent", base_revision: 1, expires_at: 1.minute.ago, - operations_json: [{"op" => "replace_exact", "old_text" => "Some content", "new_text" => "Updated content", "resolved_range" => [16, 28], "new_range" => [16, 32], "delta" => 4}], + operations_json: [ { "op" => "replace_exact", "old_text" => "Some content", "new_text" => "Updated content", "resolved_range" => [ 16, 28 ], "new_range" => [ 16, 32 ], "delta" => 4 } ], draft_content: "# Test Plan\n\nUpdated content here." ) @@ -64,7 +64,7 @@ session = CoPlan::EditSession.create!( plan: plan, actor_type: "local_agent", base_revision: 1, expires_at: 1.minute.ago, - operations_json: [{"op" => "replace_exact", "old_text" => "Some content", "new_text" => "Changed", "resolved_range" => [16, 28], "new_range" => [16, 23], "delta" => -5}], + operations_json: [ { "op" => "replace_exact", "old_text" => "Some content", "new_text" => "Changed", "resolved_range" => [ 16, 28 ], "new_range" => [ 16, 23 ], "delta" => -5 } ], draft_content: "# Test Plan\n\nChanged here." ) @@ -74,7 +74,7 @@ plan: plan, revision: 2, content_markdown: new_content, actor_type: "human", actor_id: user.id, - operations_json: [{"op" => "replace_exact", "resolved_range" => [16, 28], "new_range" => [16, 35], "delta" => 7}] + operations_json: [ { "op" => "replace_exact", "resolved_range" => [ 16, 28 ], "new_range" => [ 16, 35 ], "delta" => 7 } ] ) plan.update!(current_plan_version: version2, current_revision: 2) diff --git a/spec/lib/coplan/analytics_spec.rb b/spec/lib/coplan/analytics_spec.rb index 7ecf924d..66f73f3d 100644 --- a/spec/lib/coplan/analytics_spec.rb +++ b/spec/lib/coplan/analytics_spec.rb @@ -2,7 +2,7 @@ RSpec.describe CoPlan::Analytics do let(:received) { [] } - let(:handler) { ->(event, payload) { received << [event, payload] } } + let(:handler) { ->(event, payload) { received << [ event, payload ] } } let(:user) { create(:coplan_user) } around do |example| @@ -48,7 +48,7 @@ it "swallows handler errors and reports them via error_reporter" do reported = [] previous_reporter = CoPlan.configuration.error_reporter - CoPlan.configuration.error_reporter = ->(exception, context) { reported << [exception, context] } + CoPlan.configuration.error_reporter = ->(exception, context) { reported << [ exception, context ] } CoPlan.configuration.track_event = ->(_event, _payload) { raise "boom" } expect { described_class.track("plan_created", user: user) }.not_to raise_error diff --git a/spec/lib/tasks/coplan_summaries_spec.rb b/spec/lib/tasks/coplan_summaries_spec.rb index 1e6d5ade..4168d194 100644 --- a/spec/lib/tasks/coplan_summaries_spec.rb +++ b/spec/lib/tasks/coplan_summaries_spec.rb @@ -46,7 +46,7 @@ clear_enqueued_jobs run_task - expect(enqueued_jobs.map { |job| job[:args].first["plan_id"] }).to eq([eligible.id]) + expect(enqueued_jobs.map { |job| job[:args].first["plan_id"] }).to eq([ eligible.id ]) end it "clears a stale sha claim on a summary-less plan so the retried job isn't debounced away" do diff --git a/spec/models/coplan/notification_spec.rb b/spec/models/coplan/notification_spec.rb index e8cf059c..d56541b1 100644 --- a/spec/models/coplan/notification_spec.rb +++ b/spec/models/coplan/notification_spec.rb @@ -36,7 +36,7 @@ }.to have_enqueued_job(CoPlan::WebPushDeliveryJob).twice notification = described_class.last - [sub_a, sub_b].each do |sub| + [ sub_a, sub_b ].each do |sub| expect(CoPlan::WebPushDeliveryJob).to have_been_enqueued.with( notification_id: notification.id, subscription_id: sub.id diff --git a/spec/models/coplan/reference_spec.rb b/spec/models/coplan/reference_spec.rb index 41e2d7c8..0fbceb75 100644 --- a/spec/models/coplan/reference_spec.rb +++ b/spec/models/coplan/reference_spec.rb @@ -97,14 +97,14 @@ extracted = create(:reference, :extracted, plan: plan, url: "https://a.com") create(:reference, plan: plan, url: "https://b.com", source: "explicit") - expect(described_class.extracted).to eq([extracted]) + expect(described_class.extracted).to eq([ extracted ]) end it ".explicit returns only explicit references" do create(:reference, :extracted, plan: plan, url: "https://a.com") explicit = create(:reference, plan: plan, url: "https://b.com", source: "explicit") - expect(described_class.explicit).to eq([explicit]) + expect(described_class.explicit).to eq([ explicit ]) end end end diff --git a/spec/models/edit_session_spec.rb b/spec/models/edit_session_spec.rb index f573b583..15103b2e 100644 --- a/spec/models/edit_session_spec.rb +++ b/spec/models/edit_session_spec.rb @@ -118,7 +118,7 @@ op = { "op" => "replace_exact", "old_text" => "old", "new_text" => "new" } session.add_operation(op) session.reload - expect(session.operations_json).to eq([op]) + expect(session.operations_json).to eq([ op ]) end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index f28b981e..c60ade5c 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -24,13 +24,13 @@ it "unread returns notifications without read_at" do unread = create(:notification, user: user, plan: plan, comment_thread: thread, read_at: nil) create(:notification, user: user, plan: plan, comment_thread: thread, read_at: Time.current) - expect(CoPlan::Notification.unread).to eq([unread]) + expect(CoPlan::Notification.unread).to eq([ unread ]) end it "read returns notifications with read_at" do create(:notification, user: user, plan: plan, comment_thread: thread, read_at: nil) read_notif = create(:notification, user: user, plan: plan, comment_thread: thread, read_at: Time.current) - expect(CoPlan::Notification.read).to eq([read_notif]) + expect(CoPlan::Notification.read).to eq([ read_notif ]) end end diff --git a/spec/models/plan_collaborator_spec.rb b/spec/models/plan_collaborator_spec.rb index c7ad4668..102a1f55 100644 --- a/spec/models/plan_collaborator_spec.rb +++ b/spec/models/plan_collaborator_spec.rb @@ -83,19 +83,19 @@ it ".authors returns only author collaborators" do author = create(:plan_collaborator, plan: plan, role: "author") create(:plan_collaborator, plan: plan, role: "reviewer") - expect(plan.plan_collaborators.authors).to eq([author]) + expect(plan.plan_collaborators.authors).to eq([ author ]) end it ".approvers returns only approver collaborators" do approver = create(:plan_collaborator, plan: plan, role: "approver") create(:plan_collaborator, plan: plan, role: "viewer") - expect(plan.plan_collaborators.approvers).to eq([approver]) + expect(plan.plan_collaborators.approvers).to eq([ approver ]) end it ".highlighted returns only highlighted collaborators" do highlighted = create(:plan_collaborator, plan: plan, role: "highlighted", highlighted_reason: "Expert") create(:plan_collaborator, plan: plan, role: "author") - expect(plan.plan_collaborators.highlighted).to eq([highlighted]) + expect(plan.plan_collaborators.highlighted).to eq([ highlighted ]) end end end diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb index d6aadcac..a3b4b446 100644 --- a/spec/models/plan_spec.rb +++ b/spec/models/plan_spec.rb @@ -117,7 +117,7 @@ :considering, created_by_user: author, title: "Quarterly Strategy Document") - plan.tags = [CoPlan::Tag.find_or_create_by!(name: "strategy")] + plan.tags = [ CoPlan::Tag.find_or_create_by!(name: "strategy") ] plan.reload expect(plan.search_text).to include("Quarterly Strategy Document") @@ -136,14 +136,14 @@ it "refreshes when a tag is added" do plan = create(:plan, :considering) expect(plan.search_text).not_to include("infrastructure") - plan.tags = [CoPlan::Tag.find_or_create_by!(name: "infrastructure")] + plan.tags = [ CoPlan::Tag.find_or_create_by!(name: "infrastructure") ] expect(plan.reload.search_text).to include("infrastructure") end it "refreshes every associated plan when a tag is renamed" do tag = CoPlan::Tag.find_or_create_by!(name: "old-name") plan = create(:plan, :considering) - plan.tags = [tag] + plan.tags = [ tag ] expect(plan.reload.search_text).to include("old-name") tag.update!(name: "new-name") @@ -156,7 +156,7 @@ # Simulates the after_commit on PlanTag running when its parent Plan # row is already gone — this happens during dependent: :destroy cascade. plan = create(:plan, :considering) - plan.tags = [CoPlan::Tag.find_or_create_by!(name: "platform")] + plan.tags = [ CoPlan::Tag.find_or_create_by!(name: "platform") ] plan_tag = plan.plan_tags.first allow(plan_tag).to receive(:plan).and_return(plan) allow(plan).to receive(:destroyed?).and_return(true) diff --git a/spec/models/plan_viewer_spec.rb b/spec/models/plan_viewer_spec.rb index d0224cf6..fcd3847c 100644 --- a/spec/models/plan_viewer_spec.rb +++ b/spec/models/plan_viewer_spec.rb @@ -34,7 +34,7 @@ CoPlan::PlanViewer.track(plan: plan, user: user) viewers = CoPlan::PlanViewer.active_viewers_for(plan) - expect(viewers).to eq([user]) + expect(viewers).to eq([ user ]) end it "excludes stale viewers" do @@ -53,8 +53,8 @@ CoPlan::PlanViewer.track(plan: plan, user: user) CoPlan::PlanViewer.track(plan: other_plan, user: other_user) - expect(CoPlan::PlanViewer.active_viewers_for(plan)).to eq([user]) - expect(CoPlan::PlanViewer.active_viewers_for(other_plan)).to eq([other_user]) + expect(CoPlan::PlanViewer.active_viewers_for(plan)).to eq([ user ]) + expect(CoPlan::PlanViewer.active_viewers_for(other_plan)).to eq([ other_user ]) end it "orders viewers alphabetically by name" do @@ -64,7 +64,7 @@ CoPlan::PlanViewer.track(plan: plan, user: zara) CoPlan::PlanViewer.track(plan: plan, user: alice) - expect(CoPlan::PlanViewer.active_viewers_for(plan)).to eq([alice, zara]) + expect(CoPlan::PlanViewer.active_viewers_for(plan)).to eq([ alice, zara ]) end end diff --git a/spec/models/search_query_spec.rb b/spec/models/search_query_spec.rb index 2bbc3f45..4a07d219 100644 --- a/spec/models/search_query_spec.rb +++ b/spec/models/search_query_spec.rb @@ -62,7 +62,7 @@ described_class.log!(user: user, query: "mine") described_class.log!(user: other_user, query: "theirs") - expect(described_class.recent_for(user).pluck(:query)).to eq(["mine"]) + expect(described_class.recent_for(user).pluck(:query)).to eq([ "mine" ]) end end end diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index b53f99b5..83aa3881 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -35,29 +35,29 @@ describe "Plan#tag_names=" do it "creates Tag and PlanTag records" do plan = create(:plan) - plan.tag_names = ["infrastructure", "api-design"] - expect(plan.tag_names).to match_array(["infrastructure", "api-design"]) + plan.tag_names = [ "infrastructure", "api-design" ] + expect(plan.tag_names).to match_array([ "infrastructure", "api-design" ]) expect(CoPlan::Tag.where(name: "infrastructure")).to exist end it "reuses existing Tag records" do create(:tag, name: "security") plan = create(:plan) - expect { plan.tag_names = ["security"] }.not_to change(CoPlan::Tag, :count) - expect(plan.tag_names).to eq(["security"]) + expect { plan.tag_names = [ "security" ] }.not_to change(CoPlan::Tag, :count) + expect(plan.tag_names).to eq([ "security" ]) end it "removes old associations when tags change" do plan = create(:plan) - plan.tag_names = ["alpha", "beta"] - plan.tag_names = ["beta", "gamma"] - expect(plan.tag_names).to match_array(["beta", "gamma"]) + plan.tag_names = [ "alpha", "beta" ] + plan.tag_names = [ "beta", "gamma" ] + expect(plan.tag_names).to match_array([ "beta", "gamma" ]) end it "handles blank and duplicate names" do plan = create(:plan) - plan.tag_names = [" infra ", "infra", "", "api"] - expect(plan.tag_names).to match_array(["infra", "api"]) + plan.tag_names = [ " infra ", "infra", "", "api" ] + expect(plan.tag_names).to match_array([ "infra", "api" ]) end end end diff --git a/spec/requests/api/v1/operations_spec.rb b/spec/requests/api/v1/operations_spec.rb index 6b72dec6..6631352e 100644 --- a/spec/requests/api/v1/operations_spec.rb +++ b/spec/requests/api/v1/operations_spec.rb @@ -42,7 +42,7 @@ params: { lease_token: "no-lease", base_revision: plan.current_revision, - operations: [{ op: "replace_exact", old_text: "x", new_text: "y", count: 1 }] + operations: [ { op: "replace_exact", old_text: "x", new_text: "y", count: 1 } ] }, headers: headers, as: :json @@ -54,7 +54,7 @@ params: { lease_token: lease_token, base_revision: 999, - operations: [{ op: "replace_exact", old_text: "x", new_text: "y", count: 1 }] + operations: [ { op: "replace_exact", old_text: "x", new_text: "y", count: 1 } ] }, headers: headers, as: :json @@ -66,7 +66,7 @@ params: { lease_token: lease_token, base_revision: plan.current_revision, - operations: [{ op: "replace_exact", old_text: "nonexistent text", new_text: "y", count: 1 }] + operations: [ { op: "replace_exact", old_text: "nonexistent text", new_text: "y", count: 1 } ] }, headers: headers, as: :json @@ -97,7 +97,7 @@ params: { lease_token: lease_token, base_revision: plan.current_revision, - operations: [{ op: "replace_section", heading: "## Missing", new_content: "x" }] + operations: [ { op: "replace_section", heading: "## Missing", new_content: "x" } ] }, headers: headers, as: :json @@ -109,7 +109,7 @@ post api_v1_plan_operations_path(plan), params: { base_revision: plan.current_revision, - operations: [{ op: "replace_exact", old_text: "Some content here.", new_text: "Direct edit.", count: 1 }] + operations: [ { op: "replace_exact", old_text: "Some content here.", new_text: "Direct edit.", count: 1 } ] }, headers: headers, as: :json diff --git a/spec/requests/api/v1/plan_types_spec.rb b/spec/requests/api/v1/plan_types_spec.rb index e9a5f591..e194f39d 100644 --- a/spec/requests/api/v1/plan_types_spec.rb +++ b/spec/requests/api/v1/plan_types_spec.rb @@ -15,18 +15,18 @@ end it "returns every plan type with its template and default tags, sorted by name" do - create(:plan_type, name: "RFC", description: "Request for comments", default_tags: ["rfc"], template_content: "# RFC\n\n## Problem\n\n## Proposal") + create(:plan_type, name: "RFC", description: "Request for comments", default_tags: [ "rfc" ], template_content: "# RFC\n\n## Problem\n\n## Proposal") create(:plan_type, name: "Design Doc", description: "For design documents") get api_v1_plan_types_path, headers: headers expect(response).to have_http_status(:success) types = JSON.parse(response.body) - expect(types.map { |t| t["name"] }).to eq(["Design Doc", "RFC"]) + expect(types.map { |t| t["name"] }).to eq([ "Design Doc", "RFC" ]) rfc = types.last expect(rfc["description"]).to eq("Request for comments") - expect(rfc["default_tags"]).to eq(["rfc"]) + expect(rfc["default_tags"]).to eq([ "rfc" ]) expect(rfc["template_content"]).to include("## Proposal") end end diff --git a/spec/requests/api/v1/plans_spec.rb b/spec/requests/api/v1/plans_spec.rb index 6ad482c3..8e0e05f5 100644 --- a/spec/requests/api/v1/plans_spec.rb +++ b/spec/requests/api/v1/plans_spec.rb @@ -174,7 +174,7 @@ describe "retyping via update" do let!(:scratchpad) { create(:plan_type, name: "Scratchpad", default_tags: []) } - let!(:design) { create(:plan_type, name: "Engineering Design", default_tags: ["design"]) } + let!(:design) { create(:plan_type, name: "Engineering Design", default_tags: [ "design" ]) } it "changes the plan's type, adopts default_tags, and logs events" do typed_plan = create(:plan, :considering, created_by_user: alice, plan_type: scratchpad) @@ -194,11 +194,11 @@ it "keeps existing tags on retype" do typed_plan = create(:plan, :considering, created_by_user: alice, plan_type: scratchpad) - typed_plan.tag_names = ["pricing"] + typed_plan.tag_names = [ "pricing" ] patch api_v1_plan_path(typed_plan), params: { plan_type: "Engineering Design" }, headers: headers, as: :json - expect(JSON.parse(response.body)["tags"]).to match_array(["pricing", "design"]) + expect(JSON.parse(response.body)["tags"]).to match_array([ "pricing", "design" ]) end it "is a no-op event-wise when the type is unchanged" do @@ -232,23 +232,23 @@ describe "tags on create" do it "applies the plan type's default_tags" do - create(:plan_type, name: "design-doc", default_tags: ["design", "architecture"]) + create(:plan_type, name: "design-doc", default_tags: [ "design", "architecture" ]) post api_v1_plans_path, params: { title: "Tagged Plan", content: "# Tagged", plan_type: "design-doc" }, headers: headers, as: :json expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)["tags"]).to match_array(["design", "architecture"]) + expect(JSON.parse(response.body)["tags"]).to match_array([ "design", "architecture" ]) end it "merges explicit tags with the type's default_tags" do - create(:plan_type, name: "design-doc", default_tags: ["design"]) - post api_v1_plans_path, params: { title: "Tagged Plan", content: "# Tagged", plan_type: "design-doc", tags: ["pricing", "design"] }, headers: headers, as: :json + create(:plan_type, name: "design-doc", default_tags: [ "design" ]) + post api_v1_plans_path, params: { title: "Tagged Plan", content: "# Tagged", plan_type: "design-doc", tags: [ "pricing", "design" ] }, headers: headers, as: :json expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)["tags"]).to match_array(["design", "pricing"]) + expect(JSON.parse(response.body)["tags"]).to match_array([ "design", "pricing" ]) end it "accepts explicit tags without a plan_type" do - post api_v1_plans_path, params: { title: "Tagged Plan", content: "# Tagged", tags: ["pricing"] }, headers: headers, as: :json + post api_v1_plans_path, params: { title: "Tagged Plan", content: "# Tagged", tags: [ "pricing" ] }, headers: headers, as: :json expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)["tags"]).to eq(["pricing"]) + expect(JSON.parse(response.body)["tags"]).to eq([ "pricing" ]) end end diff --git a/spec/requests/api/v1/sessions_spec.rb b/spec/requests/api/v1/sessions_spec.rb index ded71bcf..b7847efc 100644 --- a/spec/requests/api/v1/sessions_spec.rb +++ b/spec/requests/api/v1/sessions_spec.rb @@ -58,7 +58,7 @@ current_content = plan.current_content result = CoPlan::Plans::ApplyOperations.call( content: current_content, - operations: [{ "op" => "replace_exact", "old_text" => "Some content here.", "new_text" => "Updated content.", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "Some content here.", "new_text" => "Updated content.", "count" => 1 } ] ) session.update!( operations_json: result[:applied], @@ -103,7 +103,7 @@ current_content = plan.current_content result = CoPlan::Plans::ApplyOperations.call( content: current_content, - operations: [{ "op" => "replace_exact", "old_text" => "Some content here.", "new_text" => "Changed.", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "Some content here.", "new_text" => "Changed.", "count" => 1 } ] ) session.update!( operations_json: result[:applied], @@ -237,14 +237,14 @@ content_markdown: intervening_content, actor_type: "human", actor_id: alice.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => "# Plan Content", "new_text" => "# Updated Plan Title", - "resolved_range" => [0, 14], - "new_range" => [0, 20], + "resolved_range" => [ 0, 14 ], + "new_range" => [ 0, 20 ], "delta" => 6 - }] + } ] ) plan.update!(current_plan_version: intervening_version, current_revision: new_rev) @@ -280,15 +280,15 @@ content_markdown: intervening_content, actor_type: "human", actor_id: alice.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => "Some content here.", "new_text" => "Completely different text.", - "resolved_range" => [16, 34], - "new_range" => [16, 41], + "resolved_range" => [ 16, 34 ], + "new_range" => [ 16, 41 ], "delta" => 7, "count" => 1 - }] + } ] ) plan.update!(current_plan_version: intervening_version, current_revision: new_rev) @@ -324,13 +324,13 @@ def create_intervening_replace(plan, old_text, new_text) v = CoPlan::PlanVersion.create!( plan: plan, revision: new_rev, content_markdown: new_content, actor_type: "human", actor_id: alice.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => old_text, "new_text" => new_text, - "resolved_range" => [pos, pos + old_text.length], - "new_range" => [pos, pos + new_text.length], + "resolved_range" => [ pos, pos + old_text.length ], + "new_range" => [ pos, pos + new_text.length ], "delta" => new_text.length - old_text.length - }] + } ] ) plan.update!(current_plan_version: v, current_revision: new_rev) end @@ -344,7 +344,7 @@ def create_intervening_replace(plan, old_text, new_text) post api_v1_plan_operations_path(rich_plan), params: { base_revision: stale_revision, - operations: [{ op: "insert_under_heading", heading: "## Goals", content: "New goal item." }] + operations: [ { op: "insert_under_heading", heading: "## Goals", content: "New goal item." } ] }, headers: headers, as: :json @@ -361,7 +361,7 @@ def create_intervening_replace(plan, old_text, new_text) post api_v1_plan_operations_path(rich_plan), params: { base_revision: stale_revision, - operations: [{ op: "insert_under_heading", heading: "## Goals", content: "\nNew goal item." }] + operations: [ { op: "insert_under_heading", heading: "## Goals", content: "\nNew goal item." } ] }, headers: headers, as: :json @@ -378,7 +378,7 @@ def create_intervening_replace(plan, old_text, new_text) post api_v1_plan_operations_path(rich_plan), params: { base_revision: stale_revision, - operations: [{ op: "delete_paragraph_containing", needle: "great things" }] + operations: [ { op: "delete_paragraph_containing", needle: "great things" } ] }, headers: headers, as: :json @@ -394,7 +394,7 @@ def create_intervening_replace(plan, old_text, new_text) post api_v1_plan_operations_path(rich_plan), params: { base_revision: stale_revision, - operations: [{ op: "delete_paragraph_containing", needle: "great things" }] + operations: [ { op: "delete_paragraph_containing", needle: "great things" } ] }, headers: headers, as: :json @@ -419,13 +419,13 @@ def create_intervening_replace(plan, old_text, new_text) v = CoPlan::PlanVersion.create!( plan: plan, revision: new_rev, content_markdown: new_content, actor_type: "human", actor_id: alice.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => old_text, "new_text" => new_text, - "resolved_range" => [pos, pos + old_text.length], - "new_range" => [pos, pos + new_text.length], + "resolved_range" => [ pos, pos + old_text.length ], + "new_range" => [ pos, pos + new_text.length ], "delta" => new_text.length - old_text.length - }] + } ] ) plan.update!(current_plan_version: v, current_revision: new_rev) end @@ -440,7 +440,7 @@ def create_intervening_replace(plan, old_text, new_text) params: { session_id: session_id, base_revision: rich_plan.current_revision, - operations: [{ op: "insert_under_heading", heading: "## Goals", content: "\nNew goal." }] + operations: [ { op: "insert_under_heading", heading: "## Goals", content: "\nNew goal." } ] }, headers: headers, as: :json expect(response).to have_http_status(:created) @@ -465,7 +465,7 @@ def create_intervening_replace(plan, old_text, new_text) params: { session_id: session_id, base_revision: rich_plan.current_revision, - operations: [{ op: "delete_paragraph_containing", needle: "great things" }] + operations: [ { op: "delete_paragraph_containing", needle: "great things" } ] }, headers: headers, as: :json expect(response).to have_http_status(:created) diff --git a/spec/requests/plan_content_editing_spec.rb b/spec/requests/plan_content_editing_spec.rb index e6f5e9f9..3d7e20d3 100644 --- a/spec/requests/plan_content_editing_spec.rb +++ b/spec/requests/plan_content_editing_spec.rb @@ -159,7 +159,7 @@ # metadata path rides update_content, not the legacy #update action. describe "PATCH update_content with metadata" do it "applies title and tag changes alongside content and logs their events" do - plan.tag_names = ["security"] + plan.tag_names = [ "security" ] plan.save! patch update_content_plan_path(plan), params: { @@ -240,7 +240,7 @@ describe "PATCH update with tags" do it "updates tags from a comma-separated list and logs events" do - plan.tag_names = ["security"] + plan.tag_names = [ "security" ] plan.save! patch plan_path(plan), params: { plan: { title: plan.title, tag_names: "security, api-design" } } @@ -262,7 +262,7 @@ end it "leaves tags untouched when the field is absent" do - plan.tag_names = ["security"] + plan.tag_names = [ "security" ] plan.save! patch plan_path(plan), params: { plan: { title: "New title" } } diff --git a/spec/requests/plan_events_spec.rb b/spec/requests/plan_events_spec.rb index a0c38bee..14927eae 100644 --- a/spec/requests/plan_events_spec.rb +++ b/spec/requests/plan_events_spec.rb @@ -132,13 +132,13 @@ let(:auth_headers) { { "Authorization" => "Bearer #{raw_token}" } } it "records events for title, archival, and tag diffs in a single request" do - plan.tag_names = ["existing"] + plan.tag_names = [ "existing" ] expect { patch "/api/v1/plans/#{plan.id}", params: { title: "API-renamed", archived: true, - tags: ["existing", "added"] + tags: [ "existing", "added" ] }, headers: auth_headers, as: :json }.to change { plan.plan_events.count }.by(3) @@ -148,11 +148,11 @@ end it "records tag_removed events for tags that disappear from the list" do - plan.tag_names = ["payments", "billing"] + plan.tag_names = [ "payments", "billing" ] expect { patch "/api/v1/plans/#{plan.id}", params: { - tags: ["payments"] + tags: [ "payments" ] }, headers: auth_headers, as: :json }.to change { plan.plan_events.where(event_type: "tag_removed").count }.by(1) @@ -163,7 +163,7 @@ it "records reference_added when a new reference is included in the update payload" do expect { patch "/api/v1/plans/#{plan.id}", params: { - references: [{ url: "https://docs.example.com/spec", title: "Spec" }] + references: [ { url: "https://docs.example.com/spec", title: "Spec" } ] }, headers: auth_headers, as: :json }.to change { plan.plan_events.where(event_type: "reference_added").count }.by(1) end @@ -178,8 +178,8 @@ v2 = create(:plan_version, plan: plan, revision: 2, created_at: 1.hour.ago) items = plan.history_items - expect(items.map(&:id)).to eq([v2.id, e1.id, v1.id]) - expect(items.map(&:history_kind)).to eq([:version, :event, :version]) + expect(items.map(&:id)).to eq([ v2.id, e1.id, v1.id ]) + expect(items.map(&:history_kind)).to eq([ :version, :event, :version ]) end end end diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb index e28b4c34..ae84b223 100644 --- a/spec/requests/users_spec.rb +++ b/spec/requests/users_spec.rb @@ -56,7 +56,7 @@ it "filters out hook results whose username doesn't exist locally" do get search_users_path, params: { q: "anything" } results = JSON.parse(response.body) - expect(results.map { |r| r["username"] }).to eq(["localp"]) + expect(results.map { |r| r["username"] }).to eq([ "localp" ]) end end end diff --git a/spec/services/coplan/comments/process_mentions_spec.rb b/spec/services/coplan/comments/process_mentions_spec.rb index 5e7ac1ee..6e6af90f 100644 --- a/spec/services/coplan/comments/process_mentions_spec.rb +++ b/spec/services/coplan/comments/process_mentions_spec.rb @@ -17,9 +17,9 @@ def make_comment(body, by: author) }.to change(CoPlan::Notification, :count).by(2) notifications = CoPlan::Notification.where(reason: "mention") - expect(notifications.pluck(:user_id)).to match_array([hampton.id, casey.id]) - expect(notifications.pluck(:plan_id).uniq).to eq([plan.id]) - expect(notifications.pluck(:comment_thread_id).uniq).to eq([thread.id]) + expect(notifications.pluck(:user_id)).to match_array([ hampton.id, casey.id ]) + expect(notifications.pluck(:plan_id).uniq).to eq([ plan.id ]) + expect(notifications.pluck(:comment_thread_id).uniq).to eq([ thread.id ]) end it "does not notify the author when they @-mention themselves" do @@ -59,7 +59,7 @@ def make_comment(body, by: author) comment.update!(body_markdown: "[@hampton](mention:hampton) and [@casey](mention:casey)") notifications = CoPlan::Notification.where(reason: "mention", comment_id: comment.id) - expect(notifications.pluck(:user_id)).to match_array([hampton.id, casey.id]) + expect(notifications.pluck(:user_id)).to match_array([ hampton.id, casey.id ]) expect(notifications.where(user_id: hampton.id).count).to eq(1) end diff --git a/spec/services/plan_types/install_defaults_spec.rb b/spec/services/plan_types/install_defaults_spec.rb index 63756e89..a0ca85af 100644 --- a/spec/services/plan_types/install_defaults_spec.rb +++ b/spec/services/plan_types/install_defaults_spec.rb @@ -82,7 +82,7 @@ it "restores blank shipped values with force" do # Scratchpad deliberately ships without a template; force means "back # to the shipped defaults", so a custom template must be cleared too. - create(:plan_type, name: "Scratchpad", template_content: "custom template", default_tags: ["wip"]) + create(:plan_type, name: "Scratchpad", template_content: "custom template", default_tags: [ "wip" ]) described_class.call(force: true) diff --git a/spec/services/plans/apply_operations_spec.rb b/spec/services/plans/apply_operations_spec.rb index b7611601..c347c5da 100644 --- a/spec/services/plans/apply_operations_spec.rb +++ b/spec/services/plans/apply_operations_spec.rb @@ -5,7 +5,7 @@ it "replaces text" do result = CoPlan::Plans::ApplyOperations.call( content: "Hello world, hello universe.", - operations: [{ "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 } ] ) expect(result[:content]).to eq("Hello planet, hello universe.") expect(result[:applied].length).to eq(1) @@ -14,7 +14,7 @@ it "replaces all occurrences with count 2" do result = CoPlan::Plans::ApplyOperations.call( content: "foo bar foo baz", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "count" => 2 }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "count" => 2 } ] ) expect(result[:content]).to eq("qux bar qux baz") end @@ -23,7 +23,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "Hello world", - operations: [{ "op" => "replace_exact", "old_text" => "missing", "new_text" => "found", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "missing", "new_text" => "found", "count" => 1 } ] ) }.to raise_error(CoPlan::Plans::OperationError, /found 0 occurrences/) end @@ -32,7 +32,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "foo foo foo", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "bar", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "bar", "count" => 1 } ] ) }.to raise_error(CoPlan::Plans::OperationError, /found 3 occurrences/) end @@ -41,7 +41,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "Hello", - operations: [{ "op" => "replace_exact", "new_text" => "Bye" }] + operations: [ { "op" => "replace_exact", "new_text" => "Bye" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /requires 'old_text'/) end @@ -52,7 +52,7 @@ content = "# Title\n\nIntro\n\n## Goals\n\nExisting goals." result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "insert_under_heading", "heading" => "## Goals", "content" => "- New goal" }] + operations: [ { "op" => "insert_under_heading", "heading" => "## Goals", "content" => "- New goal" } ] ) expect(result[:content]).to include("## Goals\n- New goal") expect(result[:content]).to include("Existing goals.") @@ -62,7 +62,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "# Title\n\nContent", - operations: [{ "op" => "insert_under_heading", "heading" => "## Missing", "content" => "stuff" }] + operations: [ { "op" => "insert_under_heading", "heading" => "## Missing", "content" => "stuff" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /no heading matching/) end @@ -72,7 +72,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "insert_under_heading", "heading" => "## Goals", "content" => "stuff" }] + operations: [ { "op" => "insert_under_heading", "heading" => "## Goals", "content" => "stuff" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /found 2 headings/) end @@ -83,7 +83,7 @@ content = "First paragraph.\n\nThis is deprecated.\n\nThird paragraph." result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "delete_paragraph_containing", "needle" => "deprecated" }] + operations: [ { "op" => "delete_paragraph_containing", "needle" => "deprecated" } ] ) expect(result[:content]).to eq("First paragraph.\n\nThird paragraph.") expect(result[:content]).not_to include("deprecated") @@ -93,7 +93,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "Some content.", - operations: [{ "op" => "delete_paragraph_containing", "needle" => "missing" }] + operations: [ { "op" => "delete_paragraph_containing", "needle" => "missing" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /no paragraph containing/) end @@ -103,7 +103,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "delete_paragraph_containing", "needle" => "deprecated" }] + operations: [ { "op" => "delete_paragraph_containing", "needle" => "deprecated" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /found 2 paragraphs/) end @@ -117,7 +117,7 @@ it "replaces an entire section including heading" do result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Goals", "new_content" => "## Goals\n\nNew goals here." }] + operations: [ { "op" => "replace_section", "heading" => "## Goals", "new_content" => "## Goals\n\nNew goals here." } ] ) expect(result[:content]).to include("## Goals\n\nNew goals here.") expect(result[:content]).not_to include("Goal 1.") @@ -127,7 +127,7 @@ it "replaces section body only when include_heading is false" do result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Goals", "new_content" => "Replaced body.", "include_heading" => false }] + operations: [ { "op" => "replace_section", "heading" => "## Goals", "new_content" => "Replaced body.", "include_heading" => false } ] ) expect(result[:content]).to include("## Goals") expect(result[:content]).to include("Replaced body.") @@ -137,7 +137,7 @@ it "separates heading from body when include_heading is false on heading-only content" do result = CoPlan::Plans::ApplyOperations.call( content: "## Solo", - operations: [{ "op" => "replace_section", "heading" => "## Solo", "new_content" => "New body.", "include_heading" => false }] + operations: [ { "op" => "replace_section", "heading" => "## Solo", "new_content" => "New body.", "include_heading" => false } ] ) expect(result[:content]).to eq("## Solo\nNew body.") end @@ -145,7 +145,7 @@ it "replaces the last section (extends to EOF)" do result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Timeline", "new_content" => "## Timeline\n\nNew timeline." }] + operations: [ { "op" => "replace_section", "heading" => "## Timeline", "new_content" => "## Timeline\n\nNew timeline." } ] ) expect(result[:content]).to include("## Timeline\n\nNew timeline.") expect(result[:content]).not_to include("Q1 2025.") @@ -156,7 +156,7 @@ fenced_content = "# Title\n\n## Real\n\nContent.\n\n```\n## Fake\n\nNot real.\n```\n\n## After\n\nMore." result = CoPlan::Plans::ApplyOperations.call( content: fenced_content, - operations: [{ "op" => "replace_section", "heading" => "## Real", "new_content" => "## Real\n\nReplaced." }] + operations: [ { "op" => "replace_section", "heading" => "## Real", "new_content" => "## Real\n\nReplaced." } ] ) # ## Fake inside code fence is NOT a section boundary, so the ## Real # section extends from ## Real all the way to ## After (including the fence) @@ -170,7 +170,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Missing", "new_content" => "x" }] + operations: [ { "op" => "replace_section", "heading" => "## Missing", "new_content" => "x" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /heading_not_found/) end @@ -180,7 +180,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: dup_content, - operations: [{ "op" => "replace_section", "heading" => "## Goals", "new_content" => "x" }] + operations: [ { "op" => "replace_section", "heading" => "## Goals", "new_content" => "x" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /ambiguous_heading/) end @@ -189,7 +189,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "new_content" => "x" }] + operations: [ { "op" => "replace_section", "new_content" => "x" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /requires 'heading'/) end @@ -198,7 +198,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Goals" }] + operations: [ { "op" => "replace_section", "heading" => "## Goals" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /requires 'new_content'/) end @@ -206,7 +206,7 @@ it "includes resolved position data" do result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "replace_section", "heading" => "## Goals", "new_content" => "## Goals\n\nNew." }] + operations: [ { "op" => "replace_section", "heading" => "## Goals", "new_content" => "## Goals\n\nNew." } ] ) applied = result[:applied][0] expect(applied["resolved_range"]).to be_an(Array) @@ -218,7 +218,7 @@ nested = "# Title\n\n## Section\n\nBody.\n\n### Subsection\n\nSub body.\n\n## Next\n\nOther." result = CoPlan::Plans::ApplyOperations.call( content: nested, - operations: [{ "op" => "replace_section", "heading" => "## Section", "new_content" => "## Section\n\nAll new." }] + operations: [ { "op" => "replace_section", "heading" => "## Section", "new_content" => "## Section\n\nAll new." } ] ) expect(result[:content]).to include("## Section\n\nAll new.") expect(result[:content]).not_to include("### Subsection") @@ -244,7 +244,7 @@ expect { CoPlan::Plans::ApplyOperations.call( content: "Hello", - operations: [{ "op" => "unknown_op" }] + operations: [ { "op" => "unknown_op" } ] ) }.to raise_error(CoPlan::Plans::OperationError, /unknown op/) end @@ -266,7 +266,7 @@ it "works with string keys" do result = CoPlan::Plans::ApplyOperations.call( content: "Hello world", - operations: [{ "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 } ] ) expect(result[:content]).to eq("Hello planet") end @@ -274,7 +274,7 @@ it "works with symbol keys" do result = CoPlan::Plans::ApplyOperations.call( content: "Hello world", - operations: [{ op: "replace_exact", old_text: "world", new_text: "planet", count: 1 }] + operations: [ { op: "replace_exact", old_text: "world", new_text: "planet", count: 1 } ] ) expect(result[:content]).to eq("Hello planet") end @@ -283,7 +283,7 @@ it "occurrence: 2 targets the second match" do result = CoPlan::Plans::ApplyOperations.call( content: "foo bar foo baz foo", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "occurrence" => 2 }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "occurrence" => 2 } ] ) expect(result[:content]).to eq("foo bar qux baz foo") end @@ -291,7 +291,7 @@ it "replace_all: true replaces all occurrences" do result = CoPlan::Plans::ApplyOperations.call( content: "foo bar foo baz foo", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "replace_all" => true }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "replace_all" => true } ] ) expect(result[:content]).to eq("qux bar qux baz qux") end @@ -299,7 +299,7 @@ it "legacy count still works for backward compat" do result = CoPlan::Plans::ApplyOperations.call( content: "foo bar foo baz", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "count" => 2 }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "qux", "count" => 2 } ] ) expect(result[:content]).to eq("qux bar qux baz") end @@ -309,31 +309,31 @@ it "single replace_exact includes resolved_range, new_range, and delta" do result = CoPlan::Plans::ApplyOperations.call( content: "Hello world", - operations: [{ "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 }] + operations: [ { "op" => "replace_exact", "old_text" => "world", "new_text" => "planet", "count" => 1 } ] ) applied = result[:applied][0] - expect(applied["resolved_range"]).to eq([6, 11]) - expect(applied["new_range"]).to eq([6, 12]) + expect(applied["resolved_range"]).to eq([ 6, 11 ]) + expect(applied["new_range"]).to eq([ 6, 12 ]) expect(applied["delta"]).to eq(1) end it "replace_all applied ops include replacements array" do result = CoPlan::Plans::ApplyOperations.call( content: "foo bar foo baz", - operations: [{ "op" => "replace_exact", "old_text" => "foo", "new_text" => "quux", "replace_all" => true }] + operations: [ { "op" => "replace_exact", "old_text" => "foo", "new_text" => "quux", "replace_all" => true } ] ) applied = result[:applied][0] expect(applied["replacements"]).to be_an(Array) expect(applied["replacements"].length).to eq(2) first = applied["replacements"][0] - expect(first["resolved_range"]).to eq([0, 3]) - expect(first["new_range"]).to eq([0, 4]) + expect(first["resolved_range"]).to eq([ 0, 3 ]) + expect(first["new_range"]).to eq([ 0, 4 ]) expect(first["delta"]).to eq(1) second = applied["replacements"][1] - expect(second["resolved_range"]).to eq([8, 11]) - expect(second["new_range"]).to eq([8, 12]) + expect(second["resolved_range"]).to eq([ 8, 11 ]) + expect(second["new_range"]).to eq([ 8, 12 ]) expect(second["delta"]).to eq(1) expect(applied["total_delta"]).to eq(2) @@ -343,7 +343,7 @@ content = "# Title\n\nIntro\n\n## Goals\n\nExisting goals." result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "insert_under_heading", "heading" => "## Goals", "content" => "- New goal" }] + operations: [ { "op" => "insert_under_heading", "heading" => "## Goals", "content" => "- New goal" } ] ) applied = result[:applied][0] expect(applied["resolved_range"]).to be_an(Array) @@ -356,7 +356,7 @@ content = "First paragraph.\n\nThis is deprecated.\n\nThird paragraph." result = CoPlan::Plans::ApplyOperations.call( content: content, - operations: [{ "op" => "delete_paragraph_containing", "needle" => "deprecated" }] + operations: [ { "op" => "delete_paragraph_containing", "needle" => "deprecated" } ] ) applied = result[:applied][0] expect(applied["resolved_range"]).to be_an(Array) @@ -376,13 +376,13 @@ expect(result[:content]).to eq("Hi world. Bye world.") first_applied = result[:applied][0] - expect(first_applied["resolved_range"]).to eq([0, 5]) - expect(first_applied["new_range"]).to eq([0, 2]) + expect(first_applied["resolved_range"]).to eq([ 0, 5 ]) + expect(first_applied["new_range"]).to eq([ 0, 2 ]) expect(first_applied["delta"]).to eq(-3) second_applied = result[:applied][1] - expect(second_applied["resolved_range"]).to eq([10, 17]) - expect(second_applied["new_range"]).to eq([10, 13]) + expect(second_applied["resolved_range"]).to eq([ 10, 17 ]) + expect(second_applied["new_range"]).to eq([ 10, 13 ]) expect(second_applied["delta"]).to eq(-4) end @@ -400,15 +400,15 @@ "op" => "replace_exact", "old_text" => "", # intentionally wrong / empty "new_text" => "x", - "_pre_resolved_ranges" => [[0, 10]] + "_pre_resolved_ranges" => [ [ 0, 10 ] ] } ] ) expect(result[:content]).to eq("xABCDEFGHIJ") expect(result[:applied][0]["delta"]).to eq(-9) # 1 - (10 - 0), NOT 1 - 0 - expect(result[:applied][0]["resolved_range"]).to eq([0, 10]) - expect(result[:applied][0]["new_range"]).to eq([0, 1]) + expect(result[:applied][0]["resolved_range"]).to eq([ 0, 10 ]) + expect(result[:applied][0]["new_range"]).to eq([ 0, 1 ]) end end end diff --git a/spec/services/plans/commit_session_spec.rb b/spec/services/plans/commit_session_spec.rb index 12aa5833..f4b81de0 100644 --- a/spec/services/plans/commit_session_spec.rb +++ b/spec/services/plans/commit_session_spec.rb @@ -32,7 +32,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: new_content = content.sub("unit tests", "integration tests") session = build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content ) @@ -61,7 +61,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: new_content = content.sub("unit tests", "integration tests") session = build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content, change_summary: "session summary" ) @@ -76,7 +76,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: new_content = content.sub("unit tests", "integration tests") session = build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content, change_summary: "session summary" ) @@ -92,7 +92,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: let(:session) do build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content ) end @@ -121,7 +121,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: it "copies actor_type and actor_id from session" do session = build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content, actor_type: "cloud_persona", actor_id: "persona-123" @@ -140,7 +140,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "Q1 2026 delivery.", "new_text" => "Q2 2026 delivery." }], + operations_json: [ { "op" => "replace_exact", "old_text" => "Q1 2026 delivery.", "new_text" => "Q2 2026 delivery." } ], draft_content: content.sub("Q1 2026 delivery.", "Q2 2026 delivery.") ) @@ -153,14 +153,14 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: CoPlan::PlanVersion.create!( plan: plan, revision: 2, content_markdown: v2_content, actor_type: "human", actor_id: user.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => old_text, "new_text" => new_text, - "resolved_range" => [start_pos, end_pos], - "new_range" => [start_pos, start_pos + new_text.length], + "resolved_range" => [ start_pos, end_pos ], + "new_range" => [ start_pos, start_pos + new_text.length ], "delta" => new_text.length - old_text.length - }] + } ] ) plan.update!(current_revision: 2, current_plan_version: CoPlan::PlanVersion.find_by(plan: plan, revision: 2)) @@ -178,14 +178,14 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session = build_session( plan: plan, base_revision: 1, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => "We should use unit tests.", "new_text" => "We should use integration tests.", - "resolved_range" => [content.index("We should use unit tests."), content.index("We should use unit tests.") + "We should use unit tests.".length], - "new_range" => [content.index("We should use unit tests."), content.index("We should use unit tests.") + "We should use integration tests.".length], + "resolved_range" => [ content.index("We should use unit tests."), content.index("We should use unit tests.") + "We should use unit tests.".length ], + "new_range" => [ content.index("We should use unit tests."), content.index("We should use unit tests.") + "We should use integration tests.".length ], "delta" => "integration tests".length - "unit tests".length - }], + } ], draft_content: content.sub("We should use unit tests.", "We should use integration tests.") ) @@ -198,14 +198,14 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: CoPlan::PlanVersion.create!( plan: plan, revision: 2, content_markdown: v2_content, actor_type: "human", actor_id: user.id, - operations_json: [{ + operations_json: [ { "op" => "replace_exact", "old_text" => old_text, "new_text" => new_text, - "resolved_range" => [start_pos, end_pos], - "new_range" => [start_pos, start_pos + new_text.length], + "resolved_range" => [ start_pos, end_pos ], + "new_range" => [ start_pos, start_pos + new_text.length ], "delta" => new_text.length - old_text.length - }] + } ] ) plan.update!(current_revision: 2, current_plan_version: CoPlan::PlanVersion.find_by(plan: plan, revision: 2)) @@ -238,7 +238,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: content.sub("unit tests", "integration tests") ) @@ -265,7 +265,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: new_content = content.sub("unit tests", "integration tests") session = build_session( plan: plan, - operations_json: [{ "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" }], + operations_json: [ { "op" => "replace_exact", "old_text" => "unit tests", "new_text" => "integration tests" } ], draft_content: new_content ) @@ -292,7 +292,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session_a = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "First section content.", "new_text" => "Updated first section." }], + operations_json: [ { "op" => "replace_exact", "old_text" => "First section content.", "new_text" => "Updated first section." } ], draft_content: content.sub("First section content.", "Updated first section.") ) @@ -300,7 +300,7 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session_b = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "Q1 2026 delivery.", "new_text" => "Q2 2026 delivery." }], + operations_json: [ { "op" => "replace_exact", "old_text" => "Q1 2026 delivery.", "new_text" => "Q2 2026 delivery." } ], draft_content: content.sub("Q1 2026 delivery.", "Q2 2026 delivery.") ) @@ -325,14 +325,14 @@ def build_session(plan:, operations_json: [], draft_content: nil, base_revision: session_a = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "We should use unit tests.", "new_text" => "We should use integration tests." }], + operations_json: [ { "op" => "replace_exact", "old_text" => "We should use unit tests.", "new_text" => "We should use integration tests." } ], draft_content: content.sub("We should use unit tests.", "We should use integration tests.") ) session_b = build_session( plan: plan, base_revision: 1, - operations_json: [{ "op" => "replace_exact", "old_text" => "We should use unit tests.", "new_text" => "We should use acceptance tests." }], + operations_json: [ { "op" => "replace_exact", "old_text" => "We should use unit tests.", "new_text" => "We should use acceptance tests." } ], draft_content: content.sub("We should use unit tests.", "We should use acceptance tests.") ) diff --git a/spec/services/plans/diff_to_operations_spec.rb b/spec/services/plans/diff_to_operations_spec.rb index f03c87a8..84ef00dc 100644 --- a/spec/services/plans/diff_to_operations_spec.rb +++ b/spec/services/plans/diff_to_operations_spec.rb @@ -11,7 +11,7 @@ def assert_roundtrip(old_content, new_content) "Roundtrip failed.\nOld:\n#{old_content.inspect}\nNew:\n#{new_content.inspect}\n" \ "Got:\n#{result[:content].inspect}\nOps:\n#{ops.inspect}" } - [ops, result] + [ ops, result ] end describe "no-op cases" do @@ -37,7 +37,7 @@ def assert_roundtrip(old_content, new_content) expect(ops[0]["op"]).to eq("replace_exact") expect(ops[0]["old_text"]).to eq("beta\n") expect(ops[0]["new_text"]).to eq("BETA\n") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[6, 11]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 6, 11 ] ]) end it "produces one op for an append at end of file" do @@ -47,7 +47,7 @@ def assert_roundtrip(old_content, new_content) expect(ops.length).to eq(1) expect(ops[0]["old_text"]).to eq("") expect(ops[0]["new_text"]).to eq("line three\n") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[old.length, old.length]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ old.length, old.length ] ]) end it "produces one op for an insert at start of file" do @@ -57,7 +57,7 @@ def assert_roundtrip(old_content, new_content) expect(ops.length).to eq(1) expect(ops[0]["old_text"]).to eq("") expect(ops[0]["new_text"]).to eq("line one\n") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[0, 0]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 0, 0 ] ]) end it "produces one op for an insert in the middle" do @@ -67,7 +67,7 @@ def assert_roundtrip(old_content, new_content) expect(ops.length).to eq(1) expect(ops[0]["old_text"]).to eq("") expect(ops[0]["new_text"]).to eq("beta\n") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[6, 6]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 6, 6 ] ]) end it "produces one op for a deletion in the middle" do @@ -77,7 +77,7 @@ def assert_roundtrip(old_content, new_content) expect(ops.length).to eq(1) expect(ops[0]["old_text"]).to eq("beta\n") expect(ops[0]["new_text"]).to eq("") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[6, 11]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 6, 11 ] ]) end it "produces one op for a deletion at end of file" do @@ -96,7 +96,7 @@ def assert_roundtrip(old_content, new_content) expect(ops.length).to eq(1) expect(ops[0]["old_text"]).to eq("a\n") expect(ops[0]["new_text"]).to eq("") - expect(ops[0]["_pre_resolved_ranges"]).to eq([[0, 2]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 0, 2 ] ]) end it "groups multiple consecutive changed lines into one hunk" do @@ -134,10 +134,10 @@ def assert_roundtrip(old_content, new_content) ops, _ = assert_roundtrip(old, new) expect(ops.length).to eq(2) # First op replaces "a\n" (positions 0..2) with "AAAAAA\n" (delta = +5) - expect(ops[0]["_pre_resolved_ranges"]).to eq([[0, 2]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 0, 2 ] ]) # Second op replaces "c\n" — originally at positions 4..6 in old, # but after op 1 those positions are at 9..11 in the working content. - expect(ops[1]["_pre_resolved_ranges"]).to eq([[9, 11]]) + expect(ops[1]["_pre_resolved_ranges"]).to eq([ [ 9, 11 ] ]) end it "shifts later ops' ranges to account for prior ops' delta shrinkage" do @@ -145,9 +145,9 @@ def assert_roundtrip(old_content, new_content) new = "X\nb\nC\n" ops, _ = assert_roundtrip(old, new) expect(ops.length).to eq(2) - expect(ops[0]["_pre_resolved_ranges"]).to eq([[0, 7]]) + expect(ops[0]["_pre_resolved_ranges"]).to eq([ [ 0, 7 ] ]) # Original "c\n" was at positions 9..11; after op 1 (delta = -5) they're at 4..6. - expect(ops[1]["_pre_resolved_ranges"]).to eq([[4, 6]]) + expect(ops[1]["_pre_resolved_ranges"]).to eq([ [ 4, 6 ] ]) end it "handles insert + change + delete in one document" do @@ -243,8 +243,8 @@ def assert_roundtrip(old_content, new_content) ops, result = assert_roundtrip(old, new) applied = result[:applied] expect(applied.length).to eq(1) - expect(applied[0]["resolved_range"]).to eq([6, 11]) - expect(applied[0]["new_range"]).to eq([6, 11]) + expect(applied[0]["resolved_range"]).to eq([ 6, 11 ]) + expect(applied[0]["new_range"]).to eq([ 6, 11 ]) expect(applied[0]["delta"]).to eq(0) end @@ -254,11 +254,11 @@ def assert_roundtrip(old_content, new_content) _, result = assert_roundtrip(old, new) applied = result[:applied] expect(applied.length).to eq(2) - expect(applied[0]["resolved_range"]).to eq([0, 2]) - expect(applied[0]["new_range"]).to eq([0, 7]) + expect(applied[0]["resolved_range"]).to eq([ 0, 2 ]) + expect(applied[0]["new_range"]).to eq([ 0, 7 ]) expect(applied[0]["delta"]).to eq(5) - expect(applied[1]["resolved_range"]).to eq([9, 11]) - expect(applied[1]["new_range"]).to eq([9, 11]) + expect(applied[1]["resolved_range"]).to eq([ 9, 11 ]) + expect(applied[1]["new_range"]).to eq([ 9, 11 ]) expect(applied[1]["delta"]).to eq(0) end end @@ -273,8 +273,8 @@ def assert_roundtrip(old_content, new_content) # Anchor on "alpha" at [0, 5] — unchanged, should survive version = double(operations_json: CoPlan::Plans::ApplyOperations.call(content: old, operations: ops)[:applied]) - transformed = CoPlan::Plans::TransformRange.transform_through_versions([0, 5], [version]) - expect(transformed).to eq([0, 5]) + transformed = CoPlan::Plans::TransformRange.transform_through_versions([ 0, 5 ], [ version ]) + expect(transformed).to eq([ 0, 5 ]) end it "shifts an anchor after the change by the delta" do @@ -285,8 +285,8 @@ def assert_roundtrip(old_content, new_content) # Anchor on "c" at [4, 5] in old — should shift +5 to [9, 10] in new applied = CoPlan::Plans::ApplyOperations.call(content: old, operations: ops)[:applied] version = double(operations_json: applied) - transformed = CoPlan::Plans::TransformRange.transform_through_versions([4, 5], [version]) - expect(transformed).to eq([9, 10]) + transformed = CoPlan::Plans::TransformRange.transform_through_versions([ 4, 5 ], [ version ]) + expect(transformed).to eq([ 9, 10 ]) end it "marks an anchor inside the changed region as conflicting" do @@ -298,7 +298,7 @@ def assert_roundtrip(old_content, new_content) applied = CoPlan::Plans::ApplyOperations.call(content: old, operations: ops)[:applied] version = double(operations_json: applied) expect { - CoPlan::Plans::TransformRange.transform_through_versions([2, 6], [version]) + CoPlan::Plans::TransformRange.transform_through_versions([ 2, 6 ], [ version ]) }.to raise_error(CoPlan::Plans::TransformRange::Conflict) end end @@ -345,7 +345,7 @@ def assert_roundtrip(old_content, new_content) it "roundtrips when many small disjoint hunks span a large file" do old_lines = Array.new(50) { |i| "line_#{i}\n" } new_lines = old_lines.dup - [3, 11, 27, 41].each { |i| new_lines[i] = "CHANGED_#{i}\n" } + [ 3, 11, 27, 41 ].each { |i| new_lines[i] = "CHANGED_#{i}\n" } assert_roundtrip(old_lines.join, new_lines.join) end end diff --git a/spec/services/plans/position_resolver_spec.rb b/spec/services/plans/position_resolver_spec.rb index b8043557..b662166d 100644 --- a/spec/services/plans/position_resolver_spec.rb +++ b/spec/services/plans/position_resolver_spec.rb @@ -53,7 +53,7 @@ result.ranges.each do |range| expect(content[range[0]...range[1]]).to eq("foo") end - expect(result.ranges.map(&:first)).to eq([0, 8, 16]) + expect(result.ranges.map(&:first)).to eq([ 0, 8, 16 ]) end end @@ -88,8 +88,8 @@ it "returns non-overlapping ranges" do result = resolve expect(result.ranges.length).to eq(2) - expect(result.ranges[0]).to eq([0, 2]) - expect(result.ranges[1]).to eq([2, 4]) + expect(result.ranges[0]).to eq([ 0, 2 ]) + expect(result.ranges[1]).to eq([ 2, 4 ]) end end @@ -339,7 +339,7 @@ it "resolves to entire document" do result = resolve range = result.ranges.first - expect(range).to eq([0, content.length]) + expect(range).to eq([ 0, content.length ]) reconstructed = content[0...range[0]] + content[range[1]..] expect(reconstructed).to eq("") end diff --git a/spec/services/plans/transform_range_spec.rb b/spec/services/plans/transform_range_spec.rb index e4c3d968..815403c2 100644 --- a/spec/services/plans/transform_range_spec.rb +++ b/spec/services/plans/transform_range_spec.rb @@ -5,173 +5,173 @@ describe "basic transform" do it "shifts range when edit is entirely before" do # Edit at [0, 5] replaces 5 chars with 8 chars (delta +3) - range = [10, 20] - edit = { "resolved_range" => [0, 5], "delta" => 3 } - expect(described_class.transform(range, edit)).to eq([13, 23]) + range = [ 10, 20 ] + edit = { "resolved_range" => [ 0, 5 ], "delta" => 3 } + expect(described_class.transform(range, edit)).to eq([ 13, 23 ]) end it "leaves range unchanged when edit is entirely after" do - range = [10, 20] - edit = { "resolved_range" => [25, 30], "delta" => 5 } - expect(described_class.transform(range, edit)).to eq([10, 20]) + range = [ 10, 20 ] + edit = { "resolved_range" => [ 25, 30 ], "delta" => 5 } + expect(described_class.transform(range, edit)).to eq([ 10, 20 ]) end it "shifts range when edit ends where range starts (e2 == s)" do - range = [10, 20] - edit = { "resolved_range" => [5, 10], "delta" => 3 } - expect(described_class.transform(range, edit)).to eq([13, 23]) + range = [ 10, 20 ] + edit = { "resolved_range" => [ 5, 10 ], "delta" => 3 } + expect(described_class.transform(range, edit)).to eq([ 13, 23 ]) end it "leaves range unchanged when edit starts where range ends (s2 == e)" do - range = [10, 20] - edit = { "resolved_range" => [20, 25], "delta" => 3 } - expect(described_class.transform(range, edit)).to eq([10, 20]) + range = [ 10, 20 ] + edit = { "resolved_range" => [ 20, 25 ], "delta" => 3 } + expect(described_class.transform(range, edit)).to eq([ 10, 20 ]) end it "raises Conflict when ranges overlap" do - range = [10, 20] - edit = { "resolved_range" => [15, 25], "delta" => 3 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 15, 25 ], "delta" => 3 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "raises Conflict when edit contains our range" do - range = [10, 20] - edit = { "resolved_range" => [5, 25], "delta" => -5 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 5, 25 ], "delta" => -5 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "raises Conflict when our range contains edit" do - range = [10, 20] - edit = { "resolved_range" => [12, 18], "delta" => 2 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 12, 18 ], "delta" => 2 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "raises Conflict on partial overlap from left" do - range = [10, 20] - edit = { "resolved_range" => [5, 15], "delta" => 1 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 5, 15 ], "delta" => 1 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "raises Conflict on partial overlap from right" do - range = [10, 20] - edit = { "resolved_range" => [15, 25], "delta" => 1 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 15, 25 ], "delta" => 1 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end end describe "delta calculations" do it "handles positive delta (replacement longer than original)" do - range = [20, 30] - edit = { "resolved_range" => [5, 10], "delta" => 7 } - expect(described_class.transform(range, edit)).to eq([27, 37]) + range = [ 20, 30 ] + edit = { "resolved_range" => [ 5, 10 ], "delta" => 7 } + expect(described_class.transform(range, edit)).to eq([ 27, 37 ]) end it "handles negative delta (replacement shorter)" do - range = [20, 30] - edit = { "resolved_range" => [5, 10], "delta" => -3 } - expect(described_class.transform(range, edit)).to eq([17, 27]) + range = [ 20, 30 ] + edit = { "resolved_range" => [ 5, 10 ], "delta" => -3 } + expect(described_class.transform(range, edit)).to eq([ 17, 27 ]) end it "handles zero delta (same length replacement) without overlap" do - range = [20, 30] - edit = { "resolved_range" => [5, 10], "delta" => 0 } - expect(described_class.transform(range, edit)).to eq([20, 30]) + range = [ 20, 30 ] + edit = { "resolved_range" => [ 5, 10 ], "delta" => 0 } + expect(described_class.transform(range, edit)).to eq([ 20, 30 ]) end it "detects overlap even with zero delta" do - range = [10, 20] - edit = { "resolved_range" => [15, 18], "delta" => 0 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 15, 18 ], "delta" => 0 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "handles large positive delta" do - range = [100, 200] - edit = { "resolved_range" => [0, 10], "delta" => 5000 } - expect(described_class.transform(range, edit)).to eq([5100, 5200]) + range = [ 100, 200 ] + edit = { "resolved_range" => [ 0, 10 ], "delta" => 5000 } + expect(described_class.transform(range, edit)).to eq([ 5100, 5200 ]) end it "handles large negative delta" do - range = [100, 200] - edit = { "resolved_range" => [0, 50], "delta" => -45 } - expect(described_class.transform(range, edit)).to eq([55, 155]) + range = [ 100, 200 ] + edit = { "resolved_range" => [ 0, 50 ], "delta" => -45 } + expect(described_class.transform(range, edit)).to eq([ 55, 155 ]) end it "derives delta from new_range" do - range = [20, 30] + range = [ 20, 30 ] # Edit replaced [5, 10] (5 chars) with new range [5, 12] (7 chars), delta = +2 - edit = { "resolved_range" => [5, 10], "new_range" => [5, 12] } - expect(described_class.transform(range, edit)).to eq([22, 32]) + edit = { "resolved_range" => [ 5, 10 ], "new_range" => [ 5, 12 ] } + expect(described_class.transform(range, edit)).to eq([ 22, 32 ]) end it "raises ArgumentError when neither delta nor new_range provided" do - range = [10, 20] - edit = { "resolved_range" => [0, 5] } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 0, 5 ] } expect { described_class.transform(range, edit) }.to raise_error(ArgumentError, /must contain/) end end describe "zero-width ranges (insert points)" do it "shifts insert point when edit is before" do - range = [10, 10] - edit = { "resolved_range" => [0, 5], "delta" => 3 } - expect(described_class.transform(range, edit)).to eq([13, 13]) + range = [ 10, 10 ] + edit = { "resolved_range" => [ 0, 5 ], "delta" => 3 } + expect(described_class.transform(range, edit)).to eq([ 13, 13 ]) end it "leaves insert point unchanged when edit is after" do - range = [10, 10] - edit = { "resolved_range" => [15, 20], "delta" => 5 } - expect(described_class.transform(range, edit)).to eq([10, 10]) + range = [ 10, 10 ] + edit = { "resolved_range" => [ 15, 20 ], "delta" => 5 } + expect(described_class.transform(range, edit)).to eq([ 10, 10 ]) end it "shifts insert point when edit ends at insert point (e2 == s)" do - range = [10, 10] - edit = { "resolved_range" => [5, 10], "delta" => 3 } - expect(described_class.transform(range, edit)).to eq([13, 13]) + range = [ 10, 10 ] + edit = { "resolved_range" => [ 5, 10 ], "delta" => 3 } + expect(described_class.transform(range, edit)).to eq([ 13, 13 ]) end it "raises Conflict when insert point is inside edit range" do - range = [10, 10] - edit = { "resolved_range" => [5, 15], "delta" => 3 } + range = [ 10, 10 ] + edit = { "resolved_range" => [ 5, 15 ], "delta" => 3 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "raises Conflict when insert point is at edit start" do - range = [10, 10] - edit = { "resolved_range" => [10, 15], "delta" => 3 } + range = [ 10, 10 ] + edit = { "resolved_range" => [ 10, 15 ], "delta" => 3 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end end describe "symbol keys" do it "works with symbol keys in edit_data" do - range = [10, 20] - edit = { resolved_range: [0, 5], delta: 3 } - expect(described_class.transform(range, edit)).to eq([13, 23]) + range = [ 10, 20 ] + edit = { resolved_range: [ 0, 5 ], delta: 3 } + expect(described_class.transform(range, edit)).to eq([ 13, 23 ]) end end describe "edge cases" do it "transforms range at document start [0, N] correctly" do - range = [0, 10] - edit = { "resolved_range" => [20, 25], "delta" => 5 } - expect(described_class.transform(range, edit)).to eq([0, 10]) + range = [ 0, 10 ] + edit = { "resolved_range" => [ 20, 25 ], "delta" => 5 } + expect(described_class.transform(range, edit)).to eq([ 0, 10 ]) end it "transforms range at document start when edit is also at start" do - range = [0, 10] - edit = { "resolved_range" => [0, 5], "delta" => 3 } + range = [ 0, 10 ] + edit = { "resolved_range" => [ 0, 5 ], "delta" => 3 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end it "transforms range at document end correctly" do - range = [990, 1000] - edit = { "resolved_range" => [0, 10], "delta" => 5 } - expect(described_class.transform(range, edit)).to eq([995, 1005]) + range = [ 990, 1000 ] + edit = { "resolved_range" => [ 0, 10 ], "delta" => 5 } + expect(described_class.transform(range, edit)).to eq([ 995, 1005 ]) end it "raises Conflict for empty edit (delta=0) overlapping range" do - range = [10, 20] - edit = { "resolved_range" => [12, 12], "delta" => 0 } + range = [ 10, 20 ] + edit = { "resolved_range" => [ 12, 12 ], "delta" => 0 } expect { described_class.transform(range, edit) }.to raise_error(described_class::Conflict) end end @@ -180,39 +180,39 @@ describe ".transform_through_versions" do it "transforms through two non-overlapping versions" do versions = [ - { operations_json: [{ "resolved_range" => [0, 5], "delta" => 3 }] }, - { operations_json: [{ "resolved_range" => [0, 3], "delta" => 2 }] } + { operations_json: [ { "resolved_range" => [ 0, 5 ], "delta" => 3 } ] }, + { operations_json: [ { "resolved_range" => [ 0, 3 ], "delta" => 2 } ] } ] # Start: [20, 30] # After v1: edit [0,5] delta +3 → [23, 33] # After v2: edit [0,3] delta +2 → [25, 35] - expect(described_class.transform_through_versions([20, 30], versions)).to eq([25, 35]) + expect(described_class.transform_through_versions([ 20, 30 ], versions)).to eq([ 25, 35 ]) end it "transforms through five intervening versions" do versions = (1..5).map do |i| - { operations_json: [{ "resolved_range" => [0, 1], "delta" => 1 }] } + { operations_json: [ { "resolved_range" => [ 0, 1 ], "delta" => 1 } ] } end # Each version adds 1 to both s and e, 5 total - expect(described_class.transform_through_versions([50, 60], versions)).to eq([55, 65]) + expect(described_class.transform_through_versions([ 50, 60 ], versions)).to eq([ 55, 65 ]) end it "transforms through ten intervening versions" do versions = (1..10).map do |i| - { operations_json: [{ "resolved_range" => [0, 2], "delta" => 3 }] } + { operations_json: [ { "resolved_range" => [ 0, 2 ], "delta" => 3 } ] } end # Each version shifts by +3, 10 total = +30 - expect(described_class.transform_through_versions([100, 200], versions)).to eq([130, 230]) + expect(described_class.transform_through_versions([ 100, 200 ], versions)).to eq([ 130, 230 ]) end it "skips version with empty operations_json" do versions = [ - { operations_json: [{ "resolved_range" => [0, 5], "delta" => 3 }] }, + { operations_json: [ { "resolved_range" => [ 0, 5 ], "delta" => 3 } ] }, { operations_json: [] }, { operations_json: nil }, - { operations_json: [{ "resolved_range" => [0, 3], "delta" => 2 }] } + { operations_json: [ { "resolved_range" => [ 0, 3 ], "delta" => 2 } ] } ] - expect(described_class.transform_through_versions([20, 30], versions)).to eq([25, 35]) + expect(described_class.transform_through_versions([ 20, 30 ], versions)).to eq([ 25, 35 ]) end it "transforms through version with replace_all (multiple replacements)" do @@ -221,8 +221,8 @@ operations_json: [ { "replacements" => [ - { "resolved_range" => [0, 3], "delta" => 2 }, - { "resolved_range" => [50, 53], "delta" => 2 } + { "resolved_range" => [ 0, 3 ], "delta" => 2 }, + { "resolved_range" => [ 50, 53 ], "delta" => 2 } ] } ] @@ -230,7 +230,7 @@ ] # Range [100, 110] is after both replacements # Replacements processed in reverse order: [50,53] delta +2 → [102, 112], then [0,3] delta +2 → [104, 114] - expect(described_class.transform_through_versions([100, 110], versions)).to eq([104, 114]) + expect(described_class.transform_through_versions([ 100, 110 ], versions)).to eq([ 104, 114 ]) end it "handles replace_all where range is between replacements" do @@ -239,8 +239,8 @@ operations_json: [ { "replacements" => [ - { "resolved_range" => [0, 3], "delta" => 2 }, - { "resolved_range" => [200, 203], "delta" => 5 } + { "resolved_range" => [ 0, 3 ], "delta" => 2 }, + { "resolved_range" => [ 200, 203 ], "delta" => 5 } ] } ] @@ -249,63 +249,63 @@ # Range [100, 110] # Reversed: [200,203] first — range is before, no shift → [100, 110] # Then [0,3] — range is after, shift by +2 → [102, 112] - expect(described_class.transform_through_versions([100, 110], versions)).to eq([102, 112]) + expect(described_class.transform_through_versions([ 100, 110 ], versions)).to eq([ 102, 112 ]) end it "works with PlanVersion-like objects" do - version = double("PlanVersion", operations_json: [{ "resolved_range" => [0, 5], "delta" => 3 }]) - expect(described_class.transform_through_versions([20, 30], [version])).to eq([23, 33]) + version = double("PlanVersion", operations_json: [ { "resolved_range" => [ 0, 5 ], "delta" => 3 } ]) + expect(described_class.transform_through_versions([ 20, 30 ], [ version ])).to eq([ 23, 33 ]) end it "works with string-keyed hashes" do versions = [ - { "operations_json" => [{ "resolved_range" => [0, 5], "delta" => 3 }] } + { "operations_json" => [ { "resolved_range" => [ 0, 5 ], "delta" => 3 } ] } ] - expect(described_class.transform_through_versions([20, 30], versions)).to eq([23, 33]) + expect(described_class.transform_through_versions([ 20, 30 ], versions)).to eq([ 23, 33 ]) end it "raises Conflict when a version's edit overlaps the range" do versions = [ - { operations_json: [{ "resolved_range" => [15, 25], "delta" => 3 }] } + { operations_json: [ { "resolved_range" => [ 15, 25 ], "delta" => 3 } ] } ] - expect { described_class.transform_through_versions([10, 20], versions) }.to raise_error(described_class::Conflict) + expect { described_class.transform_through_versions([ 10, 20 ], versions) }.to raise_error(described_class::Conflict) end end describe "commutativity property" do it "produces the same result for non-overlapping edits regardless of order" do - edit_a = { "resolved_range" => [0, 5], "delta" => 3 } - edit_b = { "resolved_range" => [50, 55], "delta" => -2 } + edit_a = { "resolved_range" => [ 0, 5 ], "delta" => 3 } + edit_b = { "resolved_range" => [ 50, 55 ], "delta" => -2 } - range = [60, 70] + range = [ 60, 70 ] # Apply A then B r1 = described_class.transform(range, edit_a) # After A: edit_b's position needs adjusting for testing commutativity, # but since both are before range and non-overlapping with each other, # the total shift should be the same - r1 = described_class.transform(r1, { "resolved_range" => [53, 58], "delta" => -2 }) + r1 = described_class.transform(r1, { "resolved_range" => [ 53, 58 ], "delta" => -2 }) # Apply B then A r2 = described_class.transform(range, edit_b) - r2 = described_class.transform(r2, { "resolved_range" => [0, 5], "delta" => 3 }) + r2 = described_class.transform(r2, { "resolved_range" => [ 0, 5 ], "delta" => 3 }) expect(r1).to eq(r2) end it "produces the same result for two edits both before the range" do - edit_a_data = { "resolved_range" => [0, 10], "delta" => 5 } - edit_b_data = { "resolved_range" => [20, 30], "delta" => -3 } + edit_a_data = { "resolved_range" => [ 0, 10 ], "delta" => 5 } + edit_b_data = { "resolved_range" => [ 20, 30 ], "delta" => -3 } - range = [50, 60] + range = [ 50, 60 ] # Order 1: A then B (B shifts by A's delta) r1 = described_class.transform(range, edit_a_data) - r1 = described_class.transform(r1, { "resolved_range" => [25, 35], "delta" => -3 }) + r1 = described_class.transform(r1, { "resolved_range" => [ 25, 35 ], "delta" => -3 }) # Order 2: B then A (A position unchanged since it's before B) r2 = described_class.transform(range, edit_b_data) - r2 = described_class.transform(r2, { "resolved_range" => [0, 10], "delta" => 5 }) + r2 = described_class.transform(r2, { "resolved_range" => [ 0, 10 ], "delta" => 5 }) expect(r1).to eq(r2) end diff --git a/spec/support/analytics_helpers.rb b/spec/support/analytics_helpers.rb index 6b51c363..7eb76efc 100644 --- a/spec/support/analytics_helpers.rb +++ b/spec/support/analytics_helpers.rb @@ -7,7 +7,7 @@ module AnalyticsHelpers def capture_analytics_events events = [] previous = CoPlan.configuration.track_event - CoPlan.configuration.track_event = ->(event, payload) { events << [event, payload] } + CoPlan.configuration.track_event = ->(event, payload) { events << [ event, payload ] } yield events ensure diff --git a/spec/system/comment_ux_spec.rb b/spec/system/comment_ux_spec.rb index 1a00ab37..38b142a9 100644 --- a/spec/system/comment_ux_spec.rb +++ b/spec/system/comment_ux_spec.rb @@ -632,7 +632,7 @@ def create_anchored_thread(plan:, anchor_text:, body:, user:) within(".thread-popover") do textarea = find("textarea[placeholder='Press r to reply']") textarea.fill_in with: "Line one" - textarea.send_keys([:shift, :enter]) + textarea.send_keys([ :shift, :enter ]) textarea.send_keys("Line two") value = textarea.value expect(value).to include("Line one") diff --git a/spec/system/human_editing_spec.rb b/spec/system/human_editing_spec.rb index 7be64e40..c8ee0f9f 100644 --- a/spec/system/human_editing_spec.rb +++ b/spec/system/human_editing_spec.rb @@ -46,7 +46,7 @@ def sign_in(user) end it "edits title and tags through the unified editor" do - plan.tag_names = ["security"] + plan.tag_names = [ "security" ] plan.save! visit edit_content_plan_path(plan) From 11dd30ed2f1ce0c895fe952e9c30ea6fc5df149a Mon Sep 17 00:00:00 2001 From: Hampton Lintorn-Catlin Date: Fri, 21 Aug 2026 15:52:37 -0500 Subject: [PATCH 2/2] Enforce RuboCop in CI Nothing was stopping the debt from coming back, so add a lint job to the CI workflow. It needs no database, so it is a checkout, setup-ruby and `bin/rubocop` -- action SHAs pinned to match the existing jobs. It runs as its own job rather than a step inside `test` so a bracket-spacing nit cannot mask whether the suite passed. Also state the db/schema.rb exclude explicitly. This one is belt and braces: rubocop-rails already excludes it via `db/*schema.rb`, which is why those 168 bracket offenses never showed up in a bare `bin/rubocop` run. Worth naming anyway, since it is the file people reach for first when they wonder why generated code is not linted. The `inherit_mode: merge` above it is load-bearing, not decoration. A bare `AllCops: Exclude:` replaces the inherited list rather than adding to it, which would have started linting bin/*, log/**, public/** and app/assets/**. Verified the change is inert: 425 target files before and after, with no file entering or leaving the set. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 18 ++++++++++++++++++ .rubocop.yml | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16f27137..864d55f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,24 @@ on: branches: [main] jobs: + # Style only, so it needs no database and no RAILS_ENV. Runs alongside the + # test jobs rather than gating them: a bracket-spacing nit should not hide + # whether the suite passes. + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Set up Ruby + uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run RuboCop + run: bin/rubocop + test: runs-on: ubuntu-latest diff --git a/.rubocop.yml b/.rubocop.yml index f9d86d4a..de3b80ce 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,20 @@ # Omakase Ruby styling for Rails inherit_gem: { rubocop-rails-omakase: rubocop.yml } +# `merge` appends to the inherited Exclude list instead of replacing it. Without +# this, the entry below would drop rubocop-rails' own excludes (bin/*, log/**, +# public/**, app/assets/**) and we would start linting all of them. +inherit_mode: + merge: + - Exclude + +AllCops: + Exclude: + # Rails regenerates this from the migrations, so any styling we applied + # would be reverted by the next `db:migrate`. Stated explicitly for the + # next reader; rubocop-rails already excludes it via `db/*schema.rb`. + - db/schema.rb + # Overwrite or add rules to create your own house style # # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`