Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/admin/plans.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
row :status
row :current_revision
row :created_by_user
row :tags
row(:tags) { |plan| plan.tag_names.join(", ") }
row :metadata
row :created_at
row :updated_at
Expand Down
31 changes: 31 additions & 0 deletions app/admin/tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
ActiveAdmin.register CoPlan::Tag, as: "Tag" do
permit_params :name

index do
selectable_column
id_column
column :name
column :plans_count
column :created_at
actions
end

show do
attributes_table do
row :id
row :name
row :plans_count
row :created_at
row :updated_at
end

panel "Plans" do
table_for resource.plans.order(updated_at: :desc) do
column :id
column :title
column :status
column :updated_at
end
end
end
end
19 changes: 19 additions & 0 deletions db/migrate/20260403213228_create_coplan_plan_types.co_plan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This migration comes from co_plan (originally 20260403000000)
class CreateCoplanPlanTypes < ActiveRecord::Migration[8.1]
def change
create_table :coplan_plan_types, id: { type: :string, limit: 36 } do |t|
t.string :name, null: false
t.text :description
t.json :default_tags
t.text :template_content
t.json :metadata
t.timestamps
end

add_index :coplan_plan_types, :name, unique: true

add_column :coplan_plans, :plan_type_id, :string, limit: 36
add_index :coplan_plans, :plan_type_id
add_foreign_key :coplan_plans, :coplan_plan_types, column: :plan_type_id
end
end
23 changes: 23 additions & 0 deletions db/migrate/20260403213229_seed_general_plan_type.co_plan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This migration comes from co_plan (originally 20260403100000)
class SeedGeneralPlanType < ActiveRecord::Migration[8.1]
def up
general_id = SecureRandom.uuid_v7
execute <<~SQL
INSERT INTO coplan_plan_types (id, name, description, default_tags, template_content, metadata, created_at, updated_at)
VALUES (#{quote(general_id)}, 'General', 'General-purpose plan', '[]', NULL, '{}', NOW(), NOW())
SQL

execute <<~SQL
UPDATE coplan_plans SET plan_type_id = #{quote(general_id)} WHERE plan_type_id IS NULL
SQL
end

def down
general = execute("SELECT id FROM coplan_plan_types WHERE name = 'General' LIMIT 1")
if general.any?
general_id = general.first[0]
execute("UPDATE coplan_plans SET plan_type_id = NULL WHERE plan_type_id = #{quote(general_id)}")
execute("DELETE FROM coplan_plan_types WHERE id = #{quote(general_id)}")
end
end
end
23 changes: 23 additions & 0 deletions db/migrate/20260403220000_create_coplan_tags.co_plan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This migration comes from co_plan (originally 20260403000000)
class CreateCoplanTags < ActiveRecord::Migration[8.1]
def change
create_table :coplan_tags, id: { type: :string, limit: 36 } do |t|
t.string :name, null: false
t.integer :plans_count, default: 0, null: false
t.timestamps
end

add_index :coplan_tags, :name, unique: true

create_table :coplan_plan_tags, id: { type: :string, limit: 36 } do |t|
t.string :plan_id, limit: 36, null: false
t.string :tag_id, limit: 36, null: false
t.timestamps
end

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
end
end
40 changes: 40 additions & 0 deletions db/migrate/20260403220001_backfill_structured_tags.co_plan.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# This migration comes from co_plan (originally 20260403100000)
class BackfillStructuredTags < ActiveRecord::Migration[8.1]
def up
# Backfill Tag + PlanTag records from JSON tags column
execute(<<~SQL).each do |row|
SELECT id, tags FROM coplan_plans WHERE tags IS NOT NULL
SQL
plan_id = row[0] || row["id"]
raw = row[1] || row["tags"]
next if raw.blank?

parsed = raw.is_a?(String) ? JSON.parse(raw) : raw
next unless parsed.is_a?(Array)

parsed.each do |name|
name = name.to_s.strip
next if name.blank?

tag_id = SecureRandom.uuid_v7
execute "INSERT IGNORE INTO coplan_tags (id, name, plans_count, created_at, updated_at) VALUES (#{quote(tag_id)}, #{quote(name)}, 0, NOW(), NOW())"
actual_tag_id = select_value("SELECT id FROM coplan_tags WHERE name = #{quote(name)}")
pt_id = SecureRandom.uuid_v7
execute "INSERT IGNORE INTO coplan_plan_tags (id, plan_id, tag_id, created_at, updated_at) VALUES (#{quote(pt_id)}, #{quote(plan_id)}, #{quote(actual_tag_id)}, NOW(), NOW())"
end
end

# Reset counter caches
execute <<~SQL
UPDATE coplan_tags SET plans_count = (
SELECT COUNT(*) FROM coplan_plan_tags WHERE coplan_plan_tags.tag_id = coplan_tags.id
)
SQL

remove_column :coplan_plans, :tags
end

def down
add_column :coplan_plans, :tags, :json
end
end
22 changes: 20 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions engine/app/controllers/coplan/api/v1/plans_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ def update
permitted = {}
permitted[:title] = params[:title] if params.key?(:title)
permitted[:status] = params[:status] if params.key?(:status)
permitted[:tags] = params[:tags] if params.key?(:tags)

@plan.tag_names = params[:tags] if params.key?(:tags)
@plan.update!(permitted)

if @plan.saved_changes?
Expand Down Expand Up @@ -86,7 +86,7 @@ def plan_json(plan)
title: plan.title,
status: plan.status,
current_revision: plan.current_revision,
tags: plan.tags,
tags: plan.tag_names,
plan_type_id: plan.plan_type_id,
plan_type_name: plan.plan_type&.name,
created_by: plan.created_by_user.name,
Expand Down
29 changes: 29 additions & 0 deletions engine/app/controllers/coplan/api/v1/tags_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module CoPlan
module Api
module V1
class TagsController < BaseController
def index
visible_plans = Plan.where.not(status: "brainstorm")
.or(Plan.where(created_by_user: current_user))

tags = Tag
.joins(:plan_tags)
.where(plan_tags: { plan_id: visible_plans.select(:id) })
.select("coplan_tags.*, COUNT(plan_tags.plan_id) AS visible_plans_count")
.group("coplan_tags.id")
.order("visible_plans_count DESC, coplan_tags.name ASC")

render json: tags.map { |t|
{
id: t.id,
name: t.name,
plans_count: t.visible_plans_count,
created_at: t.created_at,
updated_at: t.updated_at
}
}
end
end
end
end
end
12 changes: 11 additions & 1 deletion engine/app/models/coplan/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ class Plan < ApplicationRecord
has_many :comment_threads, dependent: :destroy
has_many :edit_sessions, dependent: :destroy
has_one :edit_lease, dependent: :destroy
has_many :plan_tags, dependent: :destroy
has_many :tags, through: :plan_tags, source: :tag
has_many :plan_viewers, dependent: :destroy
has_many :notifications, dependent: :destroy

after_initialize { self.tags ||= [] }
after_initialize { self.metadata ||= {} }

validates :title, presence: true
Expand All @@ -35,5 +36,14 @@ def to_param
def current_content
current_plan_version&.content_markdown
end

def tag_names
tags.pluck(:name)
end

def tag_names=(names)
desired = Array(names).map(&:strip).reject(&:blank?).uniq
self.tags = desired.map { |name| Tag.find_or_create_by!(name: name) }
end
end
end
16 changes: 16 additions & 0 deletions engine/app/models/coplan/plan_tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module CoPlan
class PlanTag < ApplicationRecord
belongs_to :plan
belongs_to :tag, counter_cache: :plans_count

validates :tag_id, uniqueness: { scope: :plan_id }

def self.ransackable_attributes(auth_object = nil)
%w[id plan_id tag_id created_at updated_at]
end

def self.ransackable_associations(auth_object = nil)
%w[plan tag]
end
end
end
16 changes: 16 additions & 0 deletions engine/app/models/coplan/tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module CoPlan
class Tag < ApplicationRecord
has_many :plan_tags, dependent: :destroy
has_many :plans, through: :plan_tags

validates :name, presence: true, uniqueness: true

def self.ransackable_attributes(auth_object = nil)
%w[id name plans_count created_at updated_at]
end

def self.ransackable_associations(auth_object = nil)
%w[plan_tags plans]
end
end
end
20 changes: 20 additions & 0 deletions engine/app/views/coplan/agent_instructions/show.text.erb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ Update plan metadata (title, status, tags). Only fields included in the request

Allowed fields: `title` (string), `status` (string), `tags` (array of strings).

### List Tags

```bash
<%= @curl %> \
"<%= @base %>/api/v1/tags" | jq .
```

Returns all tags sorted by usage frequency (most-used first). Each tag includes `id`, `name`, `plans_count`.

### Tags

Tags categorize plans for discovery. Use them to help people find related plans.

**Guidelines:**
- Check existing tags with `GET /api/v1/tags` before creating new ones — reuse over invention.
- Use lowercase, hyphenated names (e.g., `infrastructure`, `api-design`, `cost-reduction`).
- Apply 1–3 tags per plan. Too many tags dilute meaning.
- Update tags via `PATCH /api/v1/plans/:id` with `{"tags": ["tag-1", "tag-2"]}`.
- Good tags describe the *domain* or *concern* (e.g., `security`, `onboarding`), not the status or team.

### Plan Statuses

Plans move through a lifecycle. **Keep the status current** — update it as the plan progresses.
Expand Down
1 change: 1 addition & 0 deletions engine/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
resources :users, only: [] do
get :search, on: :collection
end
resources :tags, only: [:index]
resources :plans, only: [:index, :show, :create, :update] do
get :versions, on: :member
get :comments, on: :member
Expand Down
22 changes: 22 additions & 0 deletions engine/db/migrate/20260403000000_create_coplan_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CreateCoplanTags < ActiveRecord::Migration[8.1]
def change
create_table :coplan_tags, id: { type: :string, limit: 36 } do |t|
t.string :name, null: false
t.integer :plans_count, default: 0, null: false
t.timestamps
end

add_index :coplan_tags, :name, unique: true

create_table :coplan_plan_tags, id: { type: :string, limit: 36 } do |t|
t.string :plan_id, limit: 36, null: false
t.string :tag_id, limit: 36, null: false
t.timestamps
end

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
end
end
Loading
Loading