aidp 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +69 -0
- data/lib/aidp/cli.rb +43 -2
- data/lib/aidp/config.rb +9 -14
- data/lib/aidp/execute/prompt_manager.rb +128 -1
- data/lib/aidp/execute/repl_macros.rb +555 -0
- data/lib/aidp/execute/work_loop_runner.rb +108 -1
- data/lib/aidp/harness/ai_decision_engine.rb +376 -0
- data/lib/aidp/harness/capability_registry.rb +273 -0
- data/lib/aidp/harness/config_schema.rb +305 -1
- data/lib/aidp/harness/configuration.rb +452 -0
- data/lib/aidp/harness/enhanced_runner.rb +7 -1
- data/lib/aidp/harness/provider_factory.rb +0 -2
- data/lib/aidp/harness/runner.rb +7 -1
- data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
- data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
- data/lib/aidp/init/devcontainer_generator.rb +274 -0
- data/lib/aidp/init/runner.rb +37 -10
- data/lib/aidp/init.rb +1 -0
- data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
- data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
- data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
- data/lib/aidp/provider_manager.rb +0 -2
- data/lib/aidp/providers/anthropic.rb +19 -0
- data/lib/aidp/setup/wizard.rb +299 -4
- data/lib/aidp/utils/devcontainer_detector.rb +166 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +72 -6
- data/lib/aidp/watch/repository_client.rb +2 -1
- data/lib/aidp.rb +0 -1
- data/templates/aidp.yml.example +128 -0
- metadata +14 -2
- data/lib/aidp/providers/macos_ui.rb +0 -102
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 95269030b5c3fe91c2cfb6e29066a4f37afbcf1903e930a377ec02dd1e8a738c
         | 
| 4 | 
            +
              data.tar.gz: 6c343d45e804a73751c99e0e63491703c1eb9ba18fb2acf4e828198e774c2c92
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: dc54a6f0b9424c18ebdd8e6df75e77f0f233db98dd59543a9dd6152eacd835d8820c437ccd220c3dbcfb43cf91bfeb47e40ce415773e8cd0e24783770b2ac033
         | 
| 7 | 
            +
              data.tar.gz: f89cf387121e7edc3c384cead9369bc6ae5be97e639969fa036978a249779152ccd7a50832897be97d7fd04f4083ccf744e23e93c1117c6ed5d1cc7d8529615d
         | 
    
        data/README.md
    CHANGED
    
    | @@ -42,6 +42,75 @@ You can re-run the wizard manually: | |
| 42 42 | 
             
            aidp --setup-config
         | 
| 43 43 | 
             
            ```
         | 
| 44 44 |  | 
| 45 | 
            +
            ## Devcontainer Support
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            AIDP provides first-class devcontainer support for sandboxed, secure AI agent execution. Devcontainers offer:
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            - **Network Security**: Strict firewall with allowlisted domains only
         | 
| 50 | 
            +
            - **Sandboxed Environment**: Isolated from your host system
         | 
| 51 | 
            +
            - **Elevated Permissions**: AI agents can run with full permissions inside the container
         | 
| 52 | 
            +
            - **Consistent Setup**: Same environment across all developers
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            ### For AIDP Development
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            This repository includes a `.devcontainer/` setup for developing AIDP itself:
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            ```bash
         | 
| 59 | 
            +
            # Open in VS Code
         | 
| 60 | 
            +
            code .
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            # Press F1 → "Dev Containers: Reopen in Container"
         | 
| 63 | 
            +
            # Container builds automatically with Ruby 3.4.5, all tools, and firewall
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            # Run tests inside container
         | 
| 66 | 
            +
            bundle exec rspec
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            # Run AIDP inside container
         | 
| 69 | 
            +
            bundle exec aidp
         | 
| 70 | 
            +
            ```
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            See [.devcontainer/README.md](.devcontainer/README.md) for complete documentation.
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            ### Generating Devcontainers for Your Projects
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            Use `aidp init` to generate a devcontainer for any project:
         | 
| 77 | 
            +
             | 
| 78 | 
            +
            ```bash
         | 
| 79 | 
            +
            # Initialize project with devcontainer
         | 
| 80 | 
            +
            aidp init
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            # When prompted:
         | 
| 83 | 
            +
            # "Generate devcontainer configuration for sandboxed development?" → Yes
         | 
| 84 | 
            +
             | 
| 85 | 
            +
            # Or use the flag directly
         | 
| 86 | 
            +
            aidp init --with-devcontainer
         | 
| 87 | 
            +
            ```
         | 
| 88 | 
            +
             | 
| 89 | 
            +
            This creates:
         | 
| 90 | 
            +
             | 
| 91 | 
            +
            - `.devcontainer/Dockerfile` - Customized for your project's language/framework
         | 
| 92 | 
            +
            - `.devcontainer/devcontainer.json` - VS Code configuration and extensions
         | 
| 93 | 
            +
            - `.devcontainer/init-firewall.sh` - Network security rules
         | 
| 94 | 
            +
            - `.devcontainer/README.md` - Setup and usage documentation
         | 
| 95 | 
            +
             | 
| 96 | 
            +
            ### Elevated Permissions in Devcontainers
         | 
| 97 | 
            +
             | 
| 98 | 
            +
            When running inside a devcontainer, you can enable elevated permissions for AI agents:
         | 
| 99 | 
            +
             | 
| 100 | 
            +
            ```yaml
         | 
| 101 | 
            +
            # aidp.yml
         | 
| 102 | 
            +
            devcontainer:
         | 
| 103 | 
            +
              enabled: true
         | 
| 104 | 
            +
              full_permissions_when_in_devcontainer: true  # Run all providers with full permissions
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              # Or enable per-provider
         | 
| 107 | 
            +
              permissions:
         | 
| 108 | 
            +
                skip_permission_checks:
         | 
| 109 | 
            +
                  - claude  # Adds --dangerously-skip-permissions for Claude Code
         | 
| 110 | 
            +
            ```
         | 
| 111 | 
            +
             | 
| 112 | 
            +
            AIDP automatically detects when it's running in a devcontainer and adjusts agent permissions accordingly. This is safe because the container is sandboxed from your host system.
         | 
| 113 | 
            +
             | 
| 45 114 | 
             
            ## Core Features
         | 
| 46 115 |  | 
| 47 116 | 
             
            ### Work Loops
         | 
    
        data/lib/aidp/cli.rb
    CHANGED
    
    | @@ -759,9 +759,10 @@ module Aidp | |
| 759 759 | 
             
                    require_relative "harness/provider_info"
         | 
| 760 760 |  | 
| 761 761 | 
             
                    provider_name = args.shift
         | 
| 762 | 
            +
             | 
| 763 | 
            +
                    # If no provider specified, show models catalog table
         | 
| 762 764 | 
             
                    unless provider_name
         | 
| 763 | 
            -
                       | 
| 764 | 
            -
                      display_message("Example: aidp providers info claude", type: :info)
         | 
| 765 | 
            +
                      run_providers_models_catalog
         | 
| 765 766 | 
             
                      return
         | 
| 766 767 | 
             
                    end
         | 
| 767 768 |  | 
| @@ -839,6 +840,46 @@ module Aidp | |
| 839 840 | 
             
                    display_message("Tip: Use --refresh to update this information", type: :muted)
         | 
| 840 841 | 
             
                  end
         | 
| 841 842 |  | 
| 843 | 
            +
                  def run_providers_models_catalog
         | 
| 844 | 
            +
                    require_relative "harness/capability_registry"
         | 
| 845 | 
            +
                    require "tty-table"
         | 
| 846 | 
            +
             | 
| 847 | 
            +
                    display_message("Models Catalog - Thinking Depth Tiers", type: :highlight)
         | 
| 848 | 
            +
                    display_message("=" * 80, type: :muted)
         | 
| 849 | 
            +
             | 
| 850 | 
            +
                    registry = Aidp::Harness::CapabilityRegistry.new
         | 
| 851 | 
            +
                    unless registry.load_catalog
         | 
| 852 | 
            +
                      display_message("No models catalog found. Create .aidp/models_catalog.yml first.", type: :error)
         | 
| 853 | 
            +
                      return
         | 
| 854 | 
            +
                    end
         | 
| 855 | 
            +
             | 
| 856 | 
            +
                    rows = []
         | 
| 857 | 
            +
                    registry.provider_names.sort.each do |provider|
         | 
| 858 | 
            +
                      models = registry.models_for_provider(provider)
         | 
| 859 | 
            +
                      models.each do |model_name, model_data|
         | 
| 860 | 
            +
                        tier = model_data["tier"] || "-"
         | 
| 861 | 
            +
                        context = model_data["context_window"] ? "#{model_data["context_window"] / 1000}k" : "-"
         | 
| 862 | 
            +
                        tools = model_data["supports_tools"] ? "yes" : "no"
         | 
| 863 | 
            +
                        cost_input = model_data["cost_per_mtok_input"]
         | 
| 864 | 
            +
                        cost = cost_input ? "$#{cost_input}/MTok" : "-"
         | 
| 865 | 
            +
             | 
| 866 | 
            +
                        rows << [provider, model_name, tier, context, tools, cost]
         | 
| 867 | 
            +
                      end
         | 
| 868 | 
            +
                    end
         | 
| 869 | 
            +
             | 
| 870 | 
            +
                    if rows.empty?
         | 
| 871 | 
            +
                      display_message("No models found in catalog", type: :info)
         | 
| 872 | 
            +
                      return
         | 
| 873 | 
            +
                    end
         | 
| 874 | 
            +
             | 
| 875 | 
            +
                    header = ["Provider", "Model", "Tier", "Context", "Tools", "Cost"]
         | 
| 876 | 
            +
                    table = TTY::Table.new(header, rows)
         | 
| 877 | 
            +
                    display_message(table.render(:basic), type: :info)
         | 
| 878 | 
            +
             | 
| 879 | 
            +
                    display_message("\n" + "=" * 80, type: :muted)
         | 
| 880 | 
            +
                    display_message("Use '/thinking show' in REPL to see current tier configuration", type: :muted)
         | 
| 881 | 
            +
                  end
         | 
| 882 | 
            +
             | 
| 842 883 | 
             
                  def run_providers_refresh_command(args)
         | 
| 843 884 | 
             
                    require_relative "harness/provider_info"
         | 
| 844 885 | 
             
                    require "tty-spinner"
         | 
    
        data/lib/aidp/config.rb
    CHANGED
    
    | @@ -15,8 +15,7 @@ module Aidp | |
| 15 15 | 
             
                    no_api_keys_required: false,
         | 
| 16 16 | 
             
                    provider_weights: {
         | 
| 17 17 | 
             
                      "cursor" => 3,
         | 
| 18 | 
            -
                      "anthropic" => 2 | 
| 19 | 
            -
                      "macos" => 1
         | 
| 18 | 
            +
                      "anthropic" => 2
         | 
| 20 19 | 
             
                    },
         | 
| 21 20 | 
             
                    circuit_breaker: {
         | 
| 22 21 | 
             
                      enabled: true,
         | 
| @@ -74,6 +73,7 @@ module Aidp | |
| 74 73 | 
             
                    cursor: {
         | 
| 75 74 | 
             
                      type: "subscription",
         | 
| 76 75 | 
             
                      priority: 1,
         | 
| 76 | 
            +
                      model_family: "auto",
         | 
| 77 77 | 
             
                      default_flags: [],
         | 
| 78 78 | 
             
                      models: ["cursor-default", "cursor-fast", "cursor-precise"],
         | 
| 79 79 | 
             
                      model_weights: {
         | 
| @@ -108,6 +108,7 @@ module Aidp | |
| 108 108 | 
             
                    anthropic: {
         | 
| 109 109 | 
             
                      type: "usage_based",
         | 
| 110 110 | 
             
                      priority: 2,
         | 
| 111 | 
            +
                      model_family: "claude",
         | 
| 111 112 | 
             
                      max_tokens: 100_000,
         | 
| 112 113 | 
             
                      default_flags: ["--dangerously-skip-permissions"],
         | 
| 113 114 | 
             
                      models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
         | 
| @@ -149,18 +150,6 @@ module Aidp | |
| 149 150 | 
             
                        enabled: true,
         | 
| 150 151 | 
             
                        metrics_interval: 60
         | 
| 151 152 | 
             
                      }
         | 
| 152 | 
            -
                    },
         | 
| 153 | 
            -
                    macos: {
         | 
| 154 | 
            -
                      type: "passthrough",
         | 
| 155 | 
            -
                      priority: 4,
         | 
| 156 | 
            -
                      underlying_service: "cursor",
         | 
| 157 | 
            -
                      models: ["cursor-chat"],
         | 
| 158 | 
            -
                      features: {
         | 
| 159 | 
            -
                        file_upload: false,
         | 
| 160 | 
            -
                        code_generation: true,
         | 
| 161 | 
            -
                        analysis: true,
         | 
| 162 | 
            -
                        interactive: true
         | 
| 163 | 
            -
                      }
         | 
| 164 153 | 
             
                    }
         | 
| 165 154 | 
             
                  },
         | 
| 166 155 | 
             
                  skills: {
         | 
| @@ -342,6 +331,12 @@ module Aidp | |
| 342 331 | 
             
                    merged[:skills] = merged[:skills].merge(symbolize_keys(skills_section))
         | 
| 343 332 | 
             
                  end
         | 
| 344 333 |  | 
| 334 | 
            +
                  # Deep merge thinking config
         | 
| 335 | 
            +
                  if config[:thinking] || config["thinking"]
         | 
| 336 | 
            +
                    thinking_section = config[:thinking] || config["thinking"]
         | 
| 337 | 
            +
                    merged[:thinking] = symbolize_keys(thinking_section)
         | 
| 338 | 
            +
                  end
         | 
| 339 | 
            +
             | 
| 345 340 | 
             
                  merged
         | 
| 346 341 | 
             
                end
         | 
| 347 342 |  | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require "fileutils"
         | 
| 4 | 
            +
            require_relative "../prompt_optimization/optimizer"
         | 
| 4 5 |  | 
| 5 6 | 
             
            module Aidp
         | 
| 6 7 | 
             
              module Execute
         | 
| @@ -9,21 +10,91 @@ module Aidp | |
| 9 10 | 
             
                # - Read/write PROMPT.md
         | 
| 10 11 | 
             
                # - Check existence
         | 
| 11 12 | 
             
                # - Archive completed prompts
         | 
| 13 | 
            +
                # - Optionally optimize prompts using intelligent fragment selection (ZFC)
         | 
| 12 14 | 
             
                class PromptManager
         | 
| 13 15 | 
             
                  PROMPT_FILENAME = "PROMPT.md"
         | 
| 14 16 | 
             
                  ARCHIVE_DIR = ".aidp/prompt_archive"
         | 
| 15 17 |  | 
| 16 | 
            -
                   | 
| 18 | 
            +
                  attr_reader :optimizer, :last_optimization_stats
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def initialize(project_dir, config: nil)
         | 
| 17 21 | 
             
                    @project_dir = project_dir
         | 
| 18 22 | 
             
                    @prompt_path = File.join(project_dir, PROMPT_FILENAME)
         | 
| 19 23 | 
             
                    @archive_dir = File.join(project_dir, ARCHIVE_DIR)
         | 
| 24 | 
            +
                    @config = config
         | 
| 25 | 
            +
                    @optimizer = nil
         | 
| 26 | 
            +
                    @last_optimization_stats = nil
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    # Initialize optimizer if enabled
         | 
| 29 | 
            +
                    if config&.respond_to?(:prompt_optimization_enabled?) && config.prompt_optimization_enabled?
         | 
| 30 | 
            +
                      @optimizer = Aidp::PromptOptimization::Optimizer.new(
         | 
| 31 | 
            +
                        project_dir: project_dir,
         | 
| 32 | 
            +
                        config: config.prompt_optimization_config
         | 
| 33 | 
            +
                      )
         | 
| 34 | 
            +
                    end
         | 
| 20 35 | 
             
                  end
         | 
| 21 36 |  | 
| 22 37 | 
             
                  # Write content to PROMPT.md
         | 
| 38 | 
            +
                  # If optimization is enabled, stores the content but doesn't write yet
         | 
| 39 | 
            +
                  # (use write_optimized instead)
         | 
| 23 40 | 
             
                  def write(content)
         | 
| 24 41 | 
             
                    File.write(@prompt_path, content)
         | 
| 25 42 | 
             
                  end
         | 
| 26 43 |  | 
| 44 | 
            +
                  # Write optimized prompt using intelligent fragment selection
         | 
| 45 | 
            +
                  #
         | 
| 46 | 
            +
                  # Uses Zero Framework Cognition to select only the most relevant fragments
         | 
| 47 | 
            +
                  # from style guides, templates, and source code based on task context.
         | 
| 48 | 
            +
                  #
         | 
| 49 | 
            +
                  # @param task_context [Hash] Context about the current task
         | 
| 50 | 
            +
                  # @option task_context [Symbol] :task_type Type of task (:feature, :bugfix, etc.)
         | 
| 51 | 
            +
                  # @option task_context [String] :description Task description
         | 
| 52 | 
            +
                  # @option task_context [Array<String>] :affected_files Files being modified
         | 
| 53 | 
            +
                  # @option task_context [String] :step_name Current work loop step
         | 
| 54 | 
            +
                  # @option task_context [Array<String>] :tags Additional context tags
         | 
| 55 | 
            +
                  # @param options [Hash] Optimization options
         | 
| 56 | 
            +
                  # @option options [Boolean] :include_metadata Include debug metadata
         | 
| 57 | 
            +
                  # @return [Boolean] True if optimization was used, false if fallback to regular write
         | 
| 58 | 
            +
                  def write_optimized(task_context, options = {})
         | 
| 59 | 
            +
                    unless @optimizer
         | 
| 60 | 
            +
                      Aidp.logger.warn("prompt_manager", "Optimization requested but not enabled")
         | 
| 61 | 
            +
                      return false
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    begin
         | 
| 65 | 
            +
                      # Use optimizer to build intelligent prompt
         | 
| 66 | 
            +
                      result = @optimizer.optimize_prompt(
         | 
| 67 | 
            +
                        task_type: task_context[:task_type],
         | 
| 68 | 
            +
                        description: task_context[:description],
         | 
| 69 | 
            +
                        affected_files: task_context[:affected_files] || [],
         | 
| 70 | 
            +
                        step_name: task_context[:step_name],
         | 
| 71 | 
            +
                        tags: task_context[:tags] || [],
         | 
| 72 | 
            +
                        options: options
         | 
| 73 | 
            +
                      )
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                      # Write optimized prompt
         | 
| 76 | 
            +
                      result.write_to_file(@prompt_path)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      # Store statistics for inspection
         | 
| 79 | 
            +
                      @last_optimization_stats = result.composition_result
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      # Log optimization results
         | 
| 82 | 
            +
                      Aidp.logger.info(
         | 
| 83 | 
            +
                        "prompt_manager",
         | 
| 84 | 
            +
                        "Optimized prompt written",
         | 
| 85 | 
            +
                        selected_fragments: result.composition_result.selected_count,
         | 
| 86 | 
            +
                        excluded_fragments: result.composition_result.excluded_count,
         | 
| 87 | 
            +
                        tokens: result.estimated_tokens,
         | 
| 88 | 
            +
                        budget_utilization: result.composition_result.budget_utilization
         | 
| 89 | 
            +
                      )
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                      true
         | 
| 92 | 
            +
                    rescue => e
         | 
| 93 | 
            +
                      Aidp.logger.error("prompt_manager", "Optimization failed, using fallback", error: e.message)
         | 
| 94 | 
            +
                      false
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 27 98 | 
             
                  # Read content from PROMPT.md
         | 
| 28 99 | 
             
                  def read
         | 
| 29 100 | 
             
                    return nil unless exists?
         | 
| @@ -57,6 +128,62 @@ module Aidp | |
| 57 128 | 
             
                  def path
         | 
| 58 129 | 
             
                    @prompt_path
         | 
| 59 130 | 
             
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  # Get optimization report for last optimization
         | 
| 133 | 
            +
                  #
         | 
| 134 | 
            +
                  # @return [String, nil] Markdown report or nil if no optimization performed
         | 
| 135 | 
            +
                  def optimization_report
         | 
| 136 | 
            +
                    return nil unless @last_optimization_stats
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    # Build report from composition result
         | 
| 139 | 
            +
                    lines = []
         | 
| 140 | 
            +
                    lines << "# Prompt Optimization Report"
         | 
| 141 | 
            +
                    lines << ""
         | 
| 142 | 
            +
                    lines << "## Statistics"
         | 
| 143 | 
            +
                    lines << "- **Selected Fragments**: #{@last_optimization_stats.selected_count}"
         | 
| 144 | 
            +
                    lines << "- **Excluded Fragments**: #{@last_optimization_stats.excluded_count}"
         | 
| 145 | 
            +
                    lines << "- **Total Tokens**: #{@last_optimization_stats.total_tokens} / #{@last_optimization_stats.budget}"
         | 
| 146 | 
            +
                    lines << "- **Budget Utilization**: #{@last_optimization_stats.budget_utilization.round(1)}%"
         | 
| 147 | 
            +
                    lines << "- **Average Relevance Score**: #{(@last_optimization_stats.average_score * 100).round(1)}%"
         | 
| 148 | 
            +
                    lines << ""
         | 
| 149 | 
            +
                    lines << "## Selected Fragments"
         | 
| 150 | 
            +
                    @last_optimization_stats.selected_fragments.each do |scored|
         | 
| 151 | 
            +
                      fragment = scored[:fragment]
         | 
| 152 | 
            +
                      score = scored[:score]
         | 
| 153 | 
            +
                      lines << "- #{fragment_name(fragment)} (#{(score * 100).round(0)}%)"
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                    lines.join("\n")
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                  # Check if optimization is enabled
         | 
| 160 | 
            +
                  #
         | 
| 161 | 
            +
                  # @return [Boolean] True if optimizer is available
         | 
| 162 | 
            +
                  def optimization_enabled?
         | 
| 163 | 
            +
                    !@optimizer.nil?
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  # Get optimizer statistics
         | 
| 167 | 
            +
                  #
         | 
| 168 | 
            +
                  # @return [Hash, nil] Statistics hash or nil if optimizer not available
         | 
| 169 | 
            +
                  def optimizer_stats
         | 
| 170 | 
            +
                    @optimizer&.statistics
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                  private
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  # Get human-readable name for a fragment
         | 
| 176 | 
            +
                  def fragment_name(fragment)
         | 
| 177 | 
            +
                    if fragment.respond_to?(:heading)
         | 
| 178 | 
            +
                      fragment.heading
         | 
| 179 | 
            +
                    elsif fragment.respond_to?(:name)
         | 
| 180 | 
            +
                      fragment.name
         | 
| 181 | 
            +
                    elsif fragment.respond_to?(:id)
         | 
| 182 | 
            +
                      fragment.id
         | 
| 183 | 
            +
                    else
         | 
| 184 | 
            +
                      "Unknown fragment"
         | 
| 185 | 
            +
                    end
         | 
| 186 | 
            +
                  end
         | 
| 60 187 | 
             
                end
         | 
| 61 188 | 
             
              end
         | 
| 62 189 | 
             
            end
         |