cc-os/plugins/os-backlog/lib/backlog/resolver.rb

91 lines
3.0 KiB
Ruby

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 <board>" - an active board to work against
# "activate <archived board>" - 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<String>] 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
return decision_for_name(@config.board) || stop_and_discuss if @config.configured?
derived_decision || stop_and_discuss
end
# @return [String] one of "use <board>" / "activate <board>" / "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