local acquisition_service_factory = require("clever_f.acquisition_service") local domain = require("clever_f.domain") local feedback_service_factory = require("clever_f.feedback_service") local policy = require("clever_f.policy") local repeat_resolver_factory = require("clever_f.repeat_resolver") local sequence_state = require("clever_f.sequence_state") local state_transitions = require("clever_f.state_transitions") local M = {} local SequenceCoordinator = {} SequenceCoordinator.__index = SequenceCoordinator M.SequenceCoordinator = SequenceCoordinator local coordinator_records = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local function descriptor_text(value) if domain.Descriptor.is(value) then return value.value end return tostring(value) end function M.validate_primary_descriptor(value) local descriptor = domain.Descriptor.try_from_string(value) if descriptor == nil then fail("clever-f: Invalid mapping '" .. descriptor_text(value) .. "'", 2) end return descriptor end function SequenceCoordinator.new(options) if SequenceCoordinator.is(options) then return options end if type(options) ~= "table" then fail("SequenceCoordinator options must be a table", 2) end local host = options.host or options local state = options.state or sequence_state.get() if not sequence_state.is(state) then fail("SequenceCoordinator state must be the plugin-global SequenceState", 2) end local transitions = options.transitions or options.state_transitions or state_transitions.new(state) local policy_service = options.policy or options.policy_service or policy.new(host) local resolver = options.repeat_resolver or options.resolver or repeat_resolver_factory.new({ state = state, transitions = transitions, policy = policy_service, clock = host, }) if type(resolver) ~= "table" or type(resolver.decide) ~= "function" then fail("SequenceCoordinator repeat resolver must provide decide", 2) end local feedback = options.feedback or options.feedback_service or feedback_service_factory.new({ host = host, state = state, transitions = transitions, policy = policy_service, }) local acquisition = options.acquisition or options.acquisition_service or acquisition_service_factory.new({ host = host, state = state, transitions = transitions, policy = policy_service, feedback = feedback, }) if type(acquisition) ~= "table" or type(acquisition.acquire) ~= "function" then fail("SequenceCoordinator acquisition service must provide acquire", 2) end local coordinator = setmetatable({}, SequenceCoordinator) coordinator_records[coordinator] = { host = host, state = state, transitions = transitions, policy = policy_service, repeat_resolver = resolver, feedback = feedback, acquisition = acquisition, } return coordinator end function SequenceCoordinator.is(value) return type(value) == "table" and coordinator_records[value] ~= nil end function SequenceCoordinator:validate_primary_descriptor(value) return M.validate_primary_descriptor(value) end local function require_primary_reader(host) if type(host) ~= "table" or type(host.read_mode) ~= "function" or type(host.read_cursor) ~= "function" or type(host.read_count) ~= "function" or type(host.read_macro_state) ~= "function" then fail("SequenceCoordinator host must provide primary action state", 3) end return host end function SequenceCoordinator:read_primary_invocation() local host = require_primary_reader(coordinator_records[self].host) local context = domain.ModeContext.from_full_mode(host:read_mode()) local position = domain.Position.coerce(host:read_cursor()) local count = domain.Count.new(host:read_count()) local macro_state = domain.MacroState.new(host:read_macro_state()) return { context = context, position = position, origin = position, count = count, macro_state = macro_state, } end function SequenceCoordinator:inspect_fold_open_policy(invocation) if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then fail("fold preflight requires primary invocation state", 2) end local host = coordinator_records[self].host if type(host.read_fold_state) ~= "function" then fail("SequenceCoordinator host must provide fold state", 2) end local fold_state = host:read_fold_state() if not domain.FoldState.is(fold_state) then fail("SequenceCoordinator host must return FoldState", 2) end return fold_state end local function fold_open_enabled(fold_state) return fold_state:opens("horizontal") or fold_state:opens("all") end function SequenceCoordinator:open_enclosing_folds(invocation, fold_state) if type(invocation) ~= "table" or not domain.Position.is(invocation.position) then fail("fold opening requires primary invocation state", 2) end if not domain.FoldState.is(fold_state) then fail("fold opening requires FoldState", 2) end if not fold_open_enabled(fold_state) then return 0 end local host = coordinator_records[self].host if type(host.open_fold) ~= "function" then fail("SequenceCoordinator host must open folds", 2) end local opened = 0 while fold_state.closed_levels > 0 do if host:open_fold(invocation.position) ~= true then break end opened = opened + 1 fold_state = self:inspect_fold_open_policy(invocation) end return opened end function SequenceCoordinator:decide_primary(invocation) if type(invocation) ~= "table" or not domain.ModeContext.is(invocation.context) or not domain.Position.is(invocation.position) or not domain.MacroState.is(invocation.macro_state) then fail("primary decision requires invocation state", 2) end return coordinator_records[self].repeat_resolver:decide( invocation.context, invocation.position, invocation.macro_state ) end function SequenceCoordinator:acquire_primary(descriptor, invocation) descriptor = self:validate_primary_descriptor(descriptor) if type(invocation) ~= "table" then fail("primary acquisition requires invocation state", 2) end return coordinator_records[self].acquisition:acquire( descriptor, invocation.context, invocation.position, invocation.count, invocation.macro_state ) end function SequenceCoordinator:fresh_primary_resolution( initiating_descriptor, acquisition_result, invocation ) initiating_descriptor = self:validate_primary_descriptor(initiating_descriptor) if type(acquisition_result) ~= "table" or acquisition_result.resolved ~= true or not domain.TargetValue.is(acquisition_result.target) or not domain.TargetPlan.is(acquisition_result.target_plan) or not domain.ResolvedMotionPlan.is(acquisition_result.motion_plan) then fail("fresh primary resolution requires acquired motion plans", 2) end return { kind = "fresh", invocation = invocation, acquisition_result = acquisition_result, target = acquisition_result.target, target_plan = acquisition_result.target_plan, motion_plan = acquisition_result.motion_plan, effective_descriptor = initiating_descriptor, skip_destination = acquisition_result.target_plan.kind == domain.TargetPlanKind.EMPTY, } end function SequenceCoordinator:resolve_acquisition(descriptor, invocation) local result = self:acquire_primary(descriptor, invocation) if type(result.has_outcome) ~= "function" then fail("AcquisitionService must return an AcquisitionResult", 2) end if result:has_outcome() then return result.outcome end return self:fresh_primary_resolution(descriptor, result, invocation) end function SequenceCoordinator:evaluate_repeat_timeout(invocation) if type(invocation) ~= "table" then fail("repeat timeout requires primary invocation state", 2) end local record = coordinator_records[self] if type(record.repeat_resolver.evaluate_timeout) ~= "function" then fail("SequenceCoordinator repeat resolver must evaluate timeout", 2) end local window = record.host:read_window() local decision, elapsed_ms, cleanup = record.repeat_resolver:evaluate_timeout(window) return { decision = decision, elapsed_ms = elapsed_ms, cleanup = cleanup, window = window, } end function SequenceCoordinator:primary(value) local descriptor = self:validate_primary_descriptor(value) local invocation = self:read_primary_invocation() invocation.fold_state = self:inspect_fold_open_policy(invocation) invocation.opened_folds = self:open_enclosing_folds( invocation, invocation.fold_state ) invocation.repeat_decision = self:decide_primary(invocation) if invocation.repeat_decision == repeat_resolver_factory.Decision.ACQUIRE then return self:resolve_acquisition(descriptor, invocation) end local timeout = self:evaluate_repeat_timeout(invocation) if timeout.decision == repeat_resolver_factory.Decision.ACQUIRE then local record = coordinator_records[self] if timeout.cleanup ~= nil then if type(record.feedback.release_transition_cleanup) ~= "function" then fail("FeedbackService must release reset cleanup", 2) end record.feedback:release_transition_cleanup(timeout.cleanup) end return self:resolve_acquisition(descriptor, invocation) end return { kind = "repeat", invocation = invocation, pressed_descriptor = descriptor, timeout = timeout, } end function M.new(options) return SequenceCoordinator.new(options) end setmetatable(M, { __call = function(_, options) return SequenceCoordinator.new(options) end, }) return M