require_relative "board_spec" require_relative "config" module Backlog # Pure repo -> board routing decision. No filesystem, no network: every # input (repo path, config contents, board inventory) is passed in. # # Decision, exactly one of: # "use " - an active board to work against # "activate " - an archived board matches; rename it back # "stop-and-discuss" - no confident match; ask the human class Resolver Decision = Struct.new(:action, :board) do def to_s action == :use || action == :activate ? "#{action} #{board}" : "stop-and-discuss" end end DEV_ROOT = File.expand_path("~/dev") CLIENTS_ROOT = File.expand_path("~/clients") SERVERS_ROOT = File.expand_path("~/servers") # Path-based Planka project heuristic, shared by the resolver's own # ambiguity gate and the `board-ensure` CLI's --project default: "Dev" # under ~/dev/*, "Clients" under ~/clients/*, "Servers" under # ~/servers/* (the servers umbrella repo — ADR: infrastructure is a # distinct domain from Dev), nil (ambiguous) otherwise. Board-ensure # always accepts an arbitrary --project name too — this heuristic only # supplies the default when none is given. # # @param repo_path [String] # @return [String, nil] def self.project_for(repo_path) path = File.expand_path(repo_path) return "Dev" if under_root?(path, DEV_ROOT) return "Clients" if under_root?(path, CLIENTS_ROOT) return "Servers" if under_root?(path, SERVERS_ROOT) nil end def self.under_root?(path, root) path == root || path.start_with?("#{root}/") end private_class_method :under_root? # @param repo_path [String] absolute path to the repo # @param config_contents [String, nil] raw contents of .cc-os/config, or nil/absent # @param boards [Array] board names known to exist on the Planka instance # (both active and archived--prefixed names) def initialize(repo_path:, config_contents:, boards:) @repo_path = File.expand_path(repo_path) @config = Config.new(config_contents) @boards = boards end # @return [Decision] def resolve configured_board = @config.planka_board return decision_for_name(configured_board) || stop_and_discuss if configured_board derived_decision || stop_and_discuss end # @return [String] one of "use " / "activate " / "stop-and-discuss" def resolve_string = resolve.to_s private def derived_decision return nil if project.nil? decision_for_name(board_name) end def decision_for_name(name) return use(name) if @boards.include?(name) return activate(name) if @boards.include?(BoardSpec.archived_name(name)) nil end def stop_and_discuss = Decision.new(:stop_and_discuss, nil) def use(name) = Decision.new(:use, name) def activate(name) = Decision.new(:activate, BoardSpec.archived_name(name)) def board_name = File.basename(@repo_path) def project = self.class.project_for(@repo_path) end end