Class: Ace::Idea::Organisms::IdeaManager

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/idea/organisms/idea_manager.rb

Overview

Orchestrates all idea CRUD operations. Entry point for idea management with config-driven root directory.

Defined Under Namespace

Classes: CreateRetriesExhaustedError

Constant Summary collapse

CREATE_RETRY_LIMIT =
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_dir: nil, config: nil) ⇒ IdeaManager

Returns a new instance of IdeaManager.

Parameters:

  • root_dir (String, nil) (defaults to: nil)

    Override root directory for ideas

  • config (Hash, nil) (defaults to: nil)

    Override configuration



25
26
27
28
# File 'lib/ace/idea/organisms/idea_manager.rb', line 25

def initialize(root_dir: nil, config: nil)
  @config = config || load_config
  @root_dir = root_dir || resolve_root_dir
end

Instance Attribute Details

#last_folder_countsObject (readonly)

Returns the value of attribute last_folder_counts.



21
22
23
# File 'lib/ace/idea/organisms/idea_manager.rb', line 21

def last_folder_counts
  @last_folder_counts
end

#last_list_totalObject (readonly)

Returns the value of attribute last_list_total.



21
22
23
# File 'lib/ace/idea/organisms/idea_manager.rb', line 21

def last_list_total
  @last_list_total
end

#root_dirString (readonly)

Get the root directory

Returns:

  • (String)

    Absolute path to ideas root



175
176
177
# File 'lib/ace/idea/organisms/idea_manager.rb', line 175

def root_dir
  @root_dir
end

Instance Method Details

#create(content = nil, title: nil, tags: [], move_to: nil, clipboard: false, llm_enhance: false) ⇒ Idea

Create a new idea

Parameters:

  • content (String, nil) (defaults to: nil)

    Idea content

  • title (String, nil) (defaults to: nil)

    Optional explicit title

  • tags (Array<String>) (defaults to: [])

    Tags

  • move_to (String, nil) (defaults to: nil)

    Target folder

  • clipboard (Boolean) (defaults to: false)

    Capture from clipboard

  • llm_enhance (Boolean) (defaults to: false)

    Enhance with LLM

Returns:

  • (Idea)

    Created idea



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ace/idea/organisms/idea_manager.rb', line 38

def create(content = nil, title: nil, tags: [], move_to: nil,
  clipboard: false, llm_enhance: false)
  ensure_root_dir
  creator = Molecules::IdeaCreator.new(root_dir: @root_dir, config: @config)
  prepared_payload = creator.prepare_create_payload(
    content,
    clipboard: clipboard,
    llm_enhance: llm_enhance
  )
  attempts = 0

  begin
    attempts += 1
    creator.create(
      nil,
      title: title,
      tags: tags,
      move_to: move_to,
      clipboard: false,
      llm_enhance: false,
      time: Time.now.utc + ((attempts - 1) * 2),
      prepared_payload: prepared_payload
    )
  rescue Molecules::IdeaCreator::IdCollisionError
    retry if attempts < CREATE_RETRY_LIMIT
    raise CreateRetriesExhaustedError,
      "Failed to create idea: unable to generate a unique ID after #{CREATE_RETRY_LIMIT} attempts"
  end
end

#create_from_clipboard(llm_enhance: false, move_to: nil) ⇒ Idea

Create an idea from clipboard

Parameters:

  • llm_enhance (Boolean) (defaults to: false)

    Enhance with LLM after clipboard capture

  • move_to (String, nil) (defaults to: nil)

    Target folder

Returns:

  • (Idea)

    Created idea



72
73
74
# File 'lib/ace/idea/organisms/idea_manager.rb', line 72

def create_from_clipboard(llm_enhance: false, move_to: nil)
  create(nil, clipboard: true, llm_enhance: llm_enhance, move_to: move_to)
end

#list(status: nil, in_folder: "next", tags: [], root: nil, filters: nil) ⇒ Array<Idea>

List ideas with optional filtering

Parameters:

  • status (String, nil) (defaults to: nil)

    Filter by status

  • in_folder (String, nil) (defaults to: "next")

    Filter by special folder (default: “next” = root items only)

  • tags (Array<String>) (defaults to: [])

    Filter by tags (any match)

  • root (String, nil) (defaults to: nil)

    Override root path (subpath within root_dir)

  • filters (Array<String>, nil) (defaults to: nil)

    Generic filter strings (e.g., [“status:pending”, “tags:ux|design”])

Returns:

  • (Array<Idea>)

    List of ideas



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ace/idea/organisms/idea_manager.rb', line 97

def list(status: nil, in_folder: "next", tags: [], root: nil, filters: nil)
  scan_root = if root
    candidate = File.expand_path(File.join(@root_dir, root))
    root_real = File.expand_path(@root_dir)
    unless candidate.start_with?(root_real + File::SEPARATOR) || candidate == root_real
      raise ArgumentError, "Path traversal detected in --root option"
    end
    candidate
  else
    @root_dir
  end
  scanner = Molecules::IdeaScanner.new(scan_root)
  scan_results = scanner.scan_in_folder(in_folder)
  @last_list_total = scanner.last_scan_total
  @last_folder_counts = scanner.last_folder_counts

  loader = Molecules::IdeaLoader.new
  ideas = scan_results.filter_map do |sr|
    loader.load(sr.dir_path, id: sr.id, special_folder: sr.special_folder)
  end

  # Apply legacy filters (backward-compatible)
  ideas = ideas.select { |i| i.status == status } if status
  ideas = filter_by_tags(ideas, tags) if tags.any?

  # Apply generic --filter specs via FilterApplier
  if filters && !filters.empty?
    filter_specs = Ace::Support::Items::Atoms::FilterParser.parse(filters)
    ideas = Ace::Support::Items::Molecules::FilterApplier.apply(ideas, filter_specs, value_accessor: method(:idea_value_accessor))
  end

  ideas
end

#show(ref) ⇒ Idea?

Show (load) a single idea by reference

Parameters:

  • ref (String)

    Full ID (6 chars) or suffix shortcut (3 chars)

Returns:

  • (Idea, nil)

    Loaded idea or nil if not found



79
80
81
82
83
84
85
86
87
88
# File 'lib/ace/idea/organisms/idea_manager.rb', line 79

def show(ref)
  resolver = Molecules::IdeaResolver.new(@root_dir)
  scan_result = resolver.resolve(ref)
  return nil unless scan_result

  loader = Molecules::IdeaLoader.new
  loader.load(scan_result.dir_path,
    id: scan_result.id,
    special_folder: scan_result.special_folder)
end

#update(ref, set: {}, add: {}, remove: {}, move_to: nil) ⇒ Idea?

Update an idea’s fields and optionally move to a folder.

Parameters:

  • ref (String)

    Idea reference

  • set (Hash) (defaults to: {})

    Fields to set (key => value)

  • add (Hash) (defaults to: {})

    Fields to add to (for arrays like tags)

  • remove (Hash) (defaults to: {})

    Fields to remove from (for arrays)

  • move_to (String, nil) (defaults to: nil)

    Target folder to move to (archive, maybe, anytime, next/root//)

Returns:

  • (Idea, nil)

    Updated idea or nil if not found



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ace/idea/organisms/idea_manager.rb', line 138

def update(ref, set: {}, add: {}, remove: {}, move_to: nil)
  scan_result = resolve_scan_result(ref)
  return nil unless scan_result

  loader = Molecules::IdeaLoader.new
  idea = loader.load(scan_result.dir_path,
    id: scan_result.id,
    special_folder: scan_result.special_folder)
  return nil unless idea

  # Apply field updates if any
  has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
  update_idea_file(idea, set: set, add: add, remove: remove) if has_field_updates

  # Apply move if requested
  current_path = idea.path
  current_special = idea.special_folder
  if move_to
    mover = Molecules::IdeaMover.new(@root_dir)
    new_path = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(move_to)
      mover.move_to_root(idea)
    else
      archive_date = parse_archive_date(idea)
      mover.move(idea, to: move_to, date: archive_date)
    end
    current_path = new_path
    current_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
      new_path, root: @root_dir
    )
  end

  # Reload and return updated idea
  loader.load(current_path, id: idea.id, special_folder: current_special)
end