"""Pure owned-processing collection and UI cleanup boundary.

Official works expose a multiset, not stable work IDs. Only a single new owned
work and its exact output inventory change may resolve a collection intent.
Nothing here executes input or treats an input acknowledgement as game success.
"""
from collections import Counter
from copy import deepcopy


def _wait(reason, receipt=None, missing=None):
    return dict(status='waiting', reason=reason, missing=missing or [reason],
        next_receipt=deepcopy(receipt), effect_evidence=[], sent_to_game=False)


def _fresh(row, snapshot):
    from .arbeit_operations import _fresh as fresh
    return fresh(row, snapshot)


def _stamp(value):
    from .arbeit_operations import _stamp as stamp
    return stamp(value)


def _counts(rows):
    if not isinstance(rows, list):
        raise ValueError('current_exact_work_multiset_required')
    result = Counter()
    for row in rows:
        name, facility, count = row.get('name'), row.get('facility'), row.get('count')
        if (not isinstance(name, str) or not name or not isinstance(facility, str) or not facility
                or type(count) is not int or count <= 0 or (name, facility) in result):
            raise ValueError('current_exact_work_multiset_invalid')
        result[(name, facility)] = count
    return result


def _pending(node, snapshot, receipt):
    pending = snapshot.get('pending_input') or {}
    saved = pending.get('planner_receipt') or {}
    if (pending.get('node_id') != node['id'] or not pending.get('operation_id') or
            pending.get('status') not in ('sent_unconfirmed', 'unknown', 'intent_persisted') or
            any(saved.get(k) != receipt.get(k) for k in ('node_id', 'phase', 'observed_at', 'child_receipt')) or
            (pending.get('input_receipt') or {}).get('attempted') is False):
        raise ValueError('owned_processing_dispatch_intent_required')
    return pending


def _owner(node, snapshot, receipt, *, current_recipe=True):
    current = snapshot.get('native_processing') or {}
    proof = snapshot.get('input_owner') or {}
    identity = proof.get('owner') or {}
    quest = snapshot.get('current_quest') or {}
    if (receipt.get('node_id') != node['id'] or any(receipt.get(k) != snapshot.get(k)
            for k in ('session_id', 'character_id'))):
        raise ValueError('processing_receipt_node_or_owner_changed')
    if (not _fresh(quest, snapshot) or quest.get('confirmed') is not True or
            quest.get('source') != 'current-root-typed-quest-collections' or
            quest.get('quest_id') != node.get('quest_id') or
            any(quest.get(k) is None for k in ('instance_id', 'accept_time', 'process_started_at'))):
        raise ValueError('processing_current_accepted_occurrence_required')
    scope = receipt.get('goal_scope') or (receipt.get('child_receipt') or {}).get('goal_scope') or {}
    if any(scope.get(k) is None or scope[k] != quest.get(k)
            for k in ('quest_id', 'instance_id', 'accept_time', 'process_started_at')):
        raise ValueError('processing_original_occurrence_changed')
    if (proof.get('continuous') is not True or proof.get('epoch') is None or
            not _fresh(dict(proof, session_id=identity.get('session_id'), character_id=identity.get('character_id')), snapshot) or
            identity.get('process_started_at') != quest['process_started_at'] or
            proof.get('epoch') != (receipt.get('child_receipt') or {}).get('input_epoch')):
        raise ValueError('processing_same_continuous_input_epoch_required')
    if current_recipe and (not _fresh(current, snapshot) or current.get('confirmed') is not True or
            current.get('source_bound') is not True or current.get('input_epoch') != proof['epoch']):
        raise ValueError('current_owned_processing_scene_required')
    return dict(node_id=node['id'], quest_id=node.get('quest_id'), step_id=node.get('step_id'),
        objective_index=node.get('objective_index'), instance_id=quest['instance_id'], accept_time=quest['accept_time'],
        process_started_at=quest['process_started_at'], input_epoch=proof['epoch'],
        session_id=snapshot['session_id'], character_id=snapshot['character_id'])


def _recipe(current):
    from .processing_observer import processing_recipe_bindings
    bindings = [r for r in processing_recipe_bindings()['bindings'] if r['route_id'] == current.get('recipe_id')]
    if len(bindings) != 1 or current.get('recipe_definition') != bindings[0]:
        raise ValueError('current_exact_pinned_processing_output_definition_required')
    binding = bindings[0]
    definition = binding['current']
    observed = current.get('current_recipe') or {}
    if (observed.get('DisplayName') != definition['display_name'] or
            observed.get('ProducedPerWork') != definition['output_per_batch'] or
            current.get('facility_id') != binding['group']):
        raise ValueError('current_official_processing_recipe_output_join_required')
    return binding


def _inventory(current, snapshot, name, category=None):
    inventory = current.get('inventory_observation') or {}
    if (not _fresh(inventory, snapshot) or inventory.get('source') != 'get_items' or
            inventory.get('complete') is not True or not isinstance(inventory.get('items'), list)):
        raise ValueError('current_complete_output_bag_inventory_required')
    rows = [r for r in inventory['items'] if r.get('name') == name and r.get('scope') == 'bag']
    categories = {r.get('category') for r in rows}
    if len(categories) > 1 or (rows and (None in categories or '' in categories)):
        raise ValueError('current_output_item_category_ambiguous')
    observed_category = next(iter(categories)) if categories else category
    if category is not None and rows and observed_category != category:
        raise ValueError('current_output_item_category_changed')
    if any(type(r.get('quantity')) is not int or r['quantity'] < 0 for r in rows):
        raise ValueError('current_output_quantity_invalid')
    return dict(source=inventory['source'], observed_at=inventory['observed_at'],
        session_id=snapshot['session_id'], character_id=snapshot['character_id'],
        name=name, category=observed_category, total_quantity=sum(r['quantity'] for r in rows),
        matched_rows=deepcopy(rows), complete_bag=True, response_sha256=inventory.get('response_sha256'),
        identity_basis='exact source output name/current unique category; static raw ID is not a CLI observed ID')


def prepare_processing_collection(node, snapshot, registration_receipt):
    """Ready only for collect-all whose sole completed work is the owned one.

    Store collection_baseline verbatim in alter.collect.child_receipt. This
    function never supplies a collect target without a registration intent.
    """
    receipt = registration_receipt or {}
    try:
        if receipt.get('phase') != 'alter.recipe_detail':
            raise ValueError('owned_processing_registration_receipt_required')
        pending = _pending(node, snapshot, receipt)
        owner = _owner(node, snapshot, receipt)
        current = snapshot['native_processing']
        binding = _recipe(current)
        definition = binding['current']
        child = receipt.get('child_receipt') or {}
        baseline = child.get('registration_baseline') or {}
        exact = (definition['display_name'], current.get('facility_name'))
        if (child.get('recipe_id') != binding['route_id'] or baseline.get('recipe_name') != exact[0] or
                baseline.get('facility_name') != exact[1] or baseline.get('same_recipe_count') != 0):
            raise ValueError('owned_registration_recipe_and_zero_same_work_baseline_required')
        if (baseline.get('source') != 'get_altering_works' or any(baseline.get(k) != owner[k]
                for k in ('session_id', 'character_id')) or _stamp(baseline['observed_at']) > _stamp(receipt['observed_at'])):
            raise ValueError('owned_registration_baseline_source_or_time_required')
        before, after = _counts(baseline.get('multiset')), _counts(current.get('work_multiset'))
        proof = current.get('works_observation') or {}
        if (not _fresh(proof, snapshot) or proof.get('source') != 'get_altering_works' or
                _stamp(proof['observed_at']) <= _stamp(receipt['observed_at']) or
                before[exact] != 0 or after-before != Counter({exact: 1}) or before-after):
            raise ValueError('owned_exact_one_registered_work_effect_required')
        completed = [r for r in current.get('current_facility_works', []) if r.get('complete') is True]
        own = current.get('current_recipe_works') or []
        if (current.get('stage') != 'collect' or len(own) != 1 or own[0].get('complete') is not True or
                (own[0].get('name'), own[0].get('facility')) != exact or completed != own):
            raise ValueError('collect_all_must_contain_only_owned_completed_work')
        inventory = _inventory(current, snapshot, definition['output_name'])
        saved = dict(owner, recipe_id=binding['route_id'], facility_id=binding['group'],
            recipe_name=exact[0], facility_name=exact[1], work_multiset=deepcopy(current['work_multiset']),
            owned_work=deepcopy(own[0]), works_observation=deepcopy(proof), output_inventory=inventory,
            output_definition=deepcopy(definition), recipe_definition=deepcopy(binding),
            source_refs=deepcopy(current.get('recipe_source_refs', [])),
            registration_receipt=deepcopy(receipt), registration_operation_id=pending['operation_id'],
            source='owned registration intent plus current official work/recipe/output bag',
            observed_at=current['observed_at'], stable_work_ids_available=False)
        return dict(status='ready', collection_baseline=saved, missing=[],
            effect_evidence=[dict(owner, source='get_altering_works', observed_at=proof['observed_at'],
                confirmed=True, owned_registered_work=deepcopy(own[0]))], sent_to_game=False)
    except (ValueError, TypeError, KeyError) as exc:
        return _wait(str(exc), receipt)


def _native_goal(node, snapshot, scope, value):
    return bool(isinstance(value, dict) and _fresh(value, snapshot) and value.get('complete') is True
        and value.get('confirmed') is True and value.get('semantics') == 'current_quest_objective_native_IsAchieved'
        and all(value.get(k) == node.get(k) for k in ('quest_id', 'step_id', 'objective_index'))
        and all(value.get(k) == scope.get(k) for k in ('instance_id', 'accept_time', 'process_started_at')))



def processing_termination_evidence(node,snapshot,receipt):
    """Current old-step termination for already owned work, never a new batch."""
    from .quest_program import load_schema
    scope=(receipt or {}).get('goal_scope') or {}
    previous=(receipt or {}).get('operation_binding') or {}
    quest=snapshot.get('current_quest') or {}
    phases=('alter.recipe_detail','alter.collect','alter.collection_received','alter.cleanup')
    identity=('quest_id','instance_id','accept_time','step_id','process_started_at')
    if ((receipt or {}).get('phase') not in phases or previous.get('confirmed') is not True
            or previous.get('dispatch_op') not in ('life.alter','inventory.hold')
            or previous.get('source')!='current-root-typed-quest-collections'
            or any(scope.get(k) is None or scope[k]!=previous.get(k) for k in identity)
            or any(previous.get(k)!=node.get(k) for k in ('quest_id','step_id','objective_index'))
            or any(previous.get(k)!=snapshot.get(k) or receipt.get(k)!=snapshot.get(k)
                   for k in ('session_id','character_id'))):return None
    if any(all(p.get(k)==node.get(k) for k in ('quest_id','step_id','objective_index'))
           for p in snapshot.get('objective_programs',[]) if isinstance(p,dict)):return None
    schema=load_schema()
    definitions=[c for c in schema['classes'] if c['namespace']+'.'+c['name']==previous.get('data_type')]
    getters=[g for c in definitions for g in c.get('getters',[]) if g['name']=='get_Type' and 'constant' in g]
    if (len(getters)!=1 or (previous.get('discriminant') or {}).get('verified') is not True
            or previous['discriminant'].get('value')!=getters[0]['constant']):return None
    if any((previous.get('source_version') or {}).get(k)!=schema[k]
           for k in ('metadata_sha256','game_assembly_sha256')):return None
    if (quest.get('source')!='current-root-typed-quest-collections' or quest.get('confirmed') is not True
            or not _fresh(quest,snapshot) or any(quest.get(k)!=scope[k] for k in identity if k!='step_id')):return None
    rows=[p for p in snapshot.get('state_proofs',[]) if p.get('source')=='current-root-typed-quest-collections'
        and _fresh(p,snapshot) and p.get('confirmed') is True and p.get('state')=='step_complete'
        and all(p.get(k)==scope[k] for k in identity)
        and all((p.get('source_version') or {}).get(k)==schema[k] for k in ('metadata_sha256','game_assembly_sha256'))
        and p.get('basis') in ('native_IsQuestStepCompletedBefore_with_current_runtime_StepIds',
                             'native_IsAllStepsCompleted_and_current_runtime_StepIds')]
    if len(rows)!=1:return None
    return dict(deepcopy(rows[0]),semantics='owned_native_step_termination',node_id=node['id'],
        individual_objective_success_inferred=False,original_binding_observed_at=previous['observed_at'])


def _goal(node, snapshot, receipt, supplied):
    """Accept an actual native scoped effect or the current common hold validator."""
    from .arbeit_operations import _inventory as hold
    scope = receipt.get('goal_scope') or {}
    native = [p for p in snapshot.get('objective_evidence', []) if _native_goal(node, snapshot, scope, p)]
    if len(native) == 1:
        return deepcopy(native[0]), True
    if node.get('op') == 'inventory.hold':
        result = hold(node, snapshot, receipt)
        if result['status'] == 'complete':
            evidence = result['effect_evidence'][0]
            if supplied is None or supplied == evidence:
                return deepcopy(evidence), False
    if supplied is not None and _native_goal(node, snapshot, scope, supplied):
        return deepcopy(supplied), True
    terminated=processing_termination_evidence(node,snapshot,receipt)
    if terminated is not None:
        return terminated, True
    return None, False


def _shortage_known(node, snapshot, receipt):
    from .arbeit_operations import _inventory as hold
    if node.get('op') == 'inventory.hold':
        return hold(node, snapshot, receipt).get('reason') == 'inventory_shortage'
    scope = receipt.get('goal_scope') or {}
    rows = [p for p in snapshot.get('objective_evidence', []) if _fresh(p, snapshot)
        and p.get('confirmed') is True and p.get('complete') is False
        and p.get('semantics') == 'current_quest_objective_native_IsAchieved'
        and all(p.get(k) == node.get(k) for k in ('quest_id', 'step_id', 'objective_index'))
        and all(p.get(k) == scope.get(k) for k in ('instance_id', 'accept_time', 'process_started_at'))]
    return len(rows) == 1 and (snapshot.get('current_quest') or {}).get('step_id') == node.get('step_id')


def _received(node, snapshot, receipt, baseline):
    _pending(node, snapshot, receipt)
    owner = _owner(node, snapshot, receipt)
    if any(baseline.get(k) != value for k, value in owner.items()):
        raise ValueError('processing_collection_baseline_owner_changed')
    current = snapshot['native_processing']
    binding = _recipe(current)
    if baseline.get('recipe_definition') != binding:
        raise ValueError('processing_collection_recipe_changed')
    before, after = _counts(baseline.get('work_multiset')), _counts(current.get('work_multiset'))
    exact = (baseline['recipe_name'], baseline['facility_name'])
    proof = current.get('works_observation') or {}
    if (not _fresh(proof, snapshot) or proof.get('source') != 'get_altering_works' or
            _stamp(proof['observed_at']) <= _stamp(receipt['observed_at']) or before[exact] != 1 or
            before-after != Counter({exact: 1}) or after-before):
        raise ValueError('processing_collection_exact_owned_work_removal_pending')
    previous = baseline['output_inventory']
    inventory = _inventory(current, snapshot, previous['name'], previous.get('category'))
    delta = inventory['total_quantity'] - previous['total_quantity']
    if _stamp(inventory['observed_at']) <= _stamp(receipt['observed_at']) or delta < baseline['output_definition']['output_per_batch']:
        raise ValueError('processing_collection_exact_output_delta_pending')
    return dict(owner, source='owned collection intent/current official work removal and exact output bag delta',
        observed_at=min(_stamp(proof['observed_at']), _stamp(inventory['observed_at'])).isoformat(),
        confirmed=True, recipe_id=baseline['recipe_id'], stable_work_ids_available=False,
        removed_work=deepcopy(baseline['owned_work']), before_multiset=deepcopy(baseline['work_multiset']),
        after_multiset=deepcopy(current['work_multiset']), inventory_before=deepcopy(previous), inventory_after=inventory,
        observed_output_delta=delta, ordinary_output_per_work=baseline['output_definition']['output_per_batch'],
        bonus_output_inferred=False, source_refs=deepcopy(baseline['source_refs']))


def plan_processing_completion(node, snapshot, receipt, goal_effect=None):
    """Resolve collect once, then resume production or close own UI to actual HUD.

    Transition receipts retain the resolved intent; callers must not erase an
    unknown collection to restart the same collect action.
    """
    try:
        if snapshot.get('stop_requested'):
            return _wait('user_stop', receipt)
        if not receipt or receipt.get('phase') not in ('alter.collect', 'alter.cleanup', 'alter.collection_received'):
            return _wait('processing_owned_collection_boundary_required', receipt)
        child = receipt.get('child_receipt') or {}
        if child.get('cleanup_mode') == 'navigation_only':
            return plan_processing_navigation_cleanup(node, snapshot, receipt, goal_effect)
        baseline = child.get('collection_baseline')
        if not isinstance(baseline, dict):
            return _wait('owned_collection_baseline_required', receipt)
        owner = _owner(node, snapshot, receipt, current_recipe=receipt['phase'] == 'alter.collect')
        if any(baseline.get(k) != value for k, value in owner.items()):
            return _wait('processing_collection_baseline_owner_changed', receipt)
        evidence = child.get('collection_evidence')
        if receipt['phase'] == 'alter.collect':
            evidence = _received(node, snapshot, receipt, baseline)
        elif not isinstance(evidence, dict) or evidence.get('confirmed') is not True:
            return _wait('owned_collection_effect_record_required', receipt)
        goal, native = _goal(node, snapshot, receipt, goal_effect)
        stored_native = child.get('goal_native') is True and child.get('goal_effect')
        if goal is None and stored_native:
            # The completed native objective is a preserved historical effect,
            # not a fresh observation. Current same-occurrence ownership above
            # remains mandatory while closing its UI on a subsequent step.
            goal, native = deepcopy(child['goal_effect']), True
        if goal is None:
            if receipt['phase'] == 'alter.cleanup':
                return _wait('processing_cleanup_current_goal_effect_required', receipt)
            if not _shortage_known(node, snapshot, receipt):
                return _wait('processing_collection_current_goal_status_required', receipt)
            current = snapshot.get('native_processing') or {}
            if not _fresh(current, snapshot) or current.get('stage') not in ('recipe_list', 'recipe_scroll', 'facility_list'):
                return _wait('processing_collection_current_successor_surface_required', receipt)
            saved = deepcopy(receipt)
            saved.update(phase='alter.collection_received', dispatch_state='effect_confirmed', observed_at=evidence['observed_at'])
            saved['child_receipt'].update(collection_evidence=deepcopy(evidence))
            return dict(status='transition', reason='owned_processing_collection_received_goal_still_short',
                next_receipt=saved, effect_evidence=[evidence], prior_effect_confirmed=True, missing=[], sent_to_game=False)
        return _cleanup(node, snapshot, receipt, baseline['recipe_id'], evidence, goal, native, mode='collection')
    except (ValueError, TypeError, KeyError) as exc:
        return _wait(str(exc), receipt)


def _cleanup(node, snapshot, receipt, recipe_id, evidence, goal, native, *, mode):
    from .material_return import plan_material_return
    quest = snapshot.get('current_quest') or {}
    child = receipt.get('child_receipt') or {}
    termination=goal.get('semantics')=='owned_native_step_termination'
    def pending(reason, missing=None):
        if receipt.get('phase') == 'alter.cleanup':
            return _wait(reason, receipt, missing)
        # Persist the proven collection/navigation and goal first. This enables
        # the caller's current cleanup observer on the next read even if no
        # close target or actual empty-panel enumeration is supplied yet.
        saved = deepcopy(receipt)
        saved.update(phase='alter.cleanup', dispatch_state='effect_confirmed',
            observed_at=evidence['observed_at'])
        saved['child_receipt'].update(cleanup_mode=mode, goal_effect=deepcopy(goal), goal_native=native and not termination, goal_termination=termination,
            return_receipt=None)
        saved['child_receipt']['collection_evidence' if mode == 'collection' else 'navigation_evidence'] = deepcopy(evidence)
        return dict(status='transition', reason=reason, missing=missing or [reason], next_receipt=saved,
            effect_evidence=[evidence, goal], prior_effect_confirmed=True, sent_to_game=False)
    if quest.get('step_id') != node.get('step_id') and not native:
        return _wait('processing_cleanup_step_changed_without_owned_native_goal_effect', receipt)
    cleanup_node = deepcopy(node)
    if native:
        cleanup_node['step_id'] = quest.get('step_id')
    cleanup_snapshot = dict(snapshot)
    ui = snapshot.get('material_return_ui') or (snapshot.get('native_processing') or {}).get('return_ui') or {}
    cleanup_snapshot['material_return_ui'] = ui
    if ui.get('source_family') != 'native_alter' or ui.get('recipe_id') != recipe_id:
        return pending('processing_cleanup_own_recipe_UI_required')
    inner = deepcopy(child.get('return_receipt'))
    rebase = None
    if native and inner and inner.get('step_id') != quest.get('step_id'):
        # Rebase only the close planner's current occurrence view. Keep the
        # original dispatched close receipt and original goal scope separately.
        rebase = dict(source='current native achieved goal plus actual same-occurrence quest step',
            original_return_receipt=deepcopy(inner), from_step=inner.get('step_id'),
            current_step=quest.get('step_id'), observed_at=quest['observed_at'])
        inner['step_id'] = quest.get('step_id')
    result = plan_material_return(cleanup_node, cleanup_snapshot, inner, recipe_id=recipe_id)
    if result['status'] == 'waiting':
        return pending(result['reason'], result.get('missing'))
    if result['status'] == 'complete':
        return dict(status='complete', reason='owned_processing_goal_and_actual_field_cleanup_confirmed',
            next_receipt=None, missing=[], effect_evidence=[evidence, goal, result['field_return_evidence']],
            cleanup_mode=mode, production_effect_confirmed=mode == 'collection', sent_to_game=False,
            individual_objective_success_verified=native and not termination,
            completion_basis='native step terminated after owned processing cleanup' if termination else 'owned processing goal and cleanup')
    saved = deepcopy(receipt)
    saved.update(phase='alter.cleanup', dispatch_state='prepared', observed_at=result['next_receipt']['observed_at'])
    saved['child_receipt'].update(cleanup_mode=mode, goal_effect=deepcopy(goal), goal_native=native and not termination, goal_termination=termination,
        return_receipt=deepcopy(result['next_receipt']))
    saved['child_receipt']['collection_evidence' if mode == 'collection' else 'navigation_evidence'] = deepcopy(evidence)
    if rebase:
        saved['child_receipt'].setdefault('cleanup_scope_transitions', []).append(rebase)
    proposal = deepcopy(result['proposal'])
    proposal['current_binding']['phase'] = 'alter.cleanup'
    return dict(result, next_receipt=saved, proposal=proposal, prior_effect_confirmed=True,
        effect_evidence=[evidence, *result.get('effect_evidence', [])])


def plan_processing_navigation_cleanup(node, snapshot, receipt, goal_effect=None):
    """Close owned menus when a goal completed before registration was sent.

    Navigation confirmation is not registration, collection, or production.
    Never accepts alter.recipe_detail (a possibly dispatched registration).
    """
    try:
        if snapshot.get('stop_requested'):
            return _wait('user_stop', receipt)
        if not isinstance(receipt, dict):
            return _wait('owned_processing_navigation_receipt_required', receipt)
        child = receipt.get('child_receipt') or {}
        continuing = receipt.get('phase') == 'alter.cleanup' and child.get('cleanup_mode') == 'navigation_only'
        owner = _owner(node, snapshot, receipt, current_recipe=not continuing)
        goal, native = _goal(node, snapshot, receipt, goal_effect)
        if goal is None and continuing and child.get('goal_native') is True:
            goal, native = deepcopy(child.get('goal_effect')), True
        if goal is None:
            return _wait('processing_navigation_current_goal_effect_required', receipt)
        recipe_id = child.get('recipe_id')
        if continuing:
            evidence = child.get('navigation_evidence')
            if not isinstance(evidence, dict) or evidence.get('confirmed') is not True:
                return _wait('owned_processing_navigation_effect_record_required', receipt)
        else:
            allowed = {'alter.field': ('hub_tabs', 'facility_list'), 'alter.hub_tabs': ('facility_list',),
                'alter.facility_list': ('recipe_list', 'recipe_scroll', 'remote_facility', 'collect'),
                'alter.recipe_list': ('recipe_detail',), 'alter.remote_facility': ('recipe_list', 'recipe_scroll', 'collect'),
                'alter.recipe_scroll': ('recipe_scroll', 'recipe_list')}
            if receipt.get('phase') not in allowed:
                return _wait('processing_navigation_cleanup_cannot_resolve_production_intent', receipt)
            _pending(node, snapshot, receipt)
            current = snapshot['native_processing']
            if (current.get('recipe_id') != recipe_id or current.get('stage') not in allowed[receipt['phase']] or
                    _stamp(current['observed_at']) <= _stamp(receipt['observed_at'])):
                return _wait('processing_navigation_actual_owned_successor_required', receipt)
            if receipt['phase'] == 'alter.recipe_scroll':
                before = child.get('scroll_observation') or {}
                after = current.get('scroll_observation') or {}
                if not before.get('digest') or not _fresh(after, snapshot) or after.get('digest') == before['digest']:
                    return _wait('processing_navigation_scroll_effect_required', receipt)
            evidence = dict(owner, recipe_id=recipe_id, source=current['source'], observed_at=current['observed_at'],
                confirmed=True, from_phase=receipt['phase'], current_stage=current['stage'],
                production_effect_confirmed=False)
        return _cleanup(node, snapshot, receipt, recipe_id, evidence, goal, native, mode='navigation_only')
    except (ValueError, TypeError, KeyError) as exc:
        return _wait(str(exc), receipt)
