"""Current crafting root/model/layout supplier. Import and default use are inert.

The game is never called or written. Read access must be explicitly enabled;
all source inspection and tests use retained files or injected sessions.
"""
from __future__ import annotations
from mini_masukja.native_type_reference import type_definition_index
from copy import deepcopy
from datetime import datetime,timezone
from functools import lru_cache
import hashlib,json,struct,math
from pathlib import Path
from .paths import root
from .processing_observer import CurrentProcessingSession,verify_processing_sheet_revision
from .ui_layout_observer import XForestView
from .quest_ui_actions import QuestUIActionSession
from .vendor.map_character_context import _class
from .vendor.map_memory import MapReadPending

@lru_cache(maxsize=1)
def craft_sources():
    folder=root()/'data/arbeit-craft'
    return {n:json.loads((folder/(n+'.json')).read_text('utf-8')) for n in ('declarations','recipes','ui-source','result-shapes')}

def recipe_candidates(node):
    """Static supply candidates, never current gameplay objective predicates."""
    targets=node.get('params',{}).get('targets') or node.get('params',{}).get('objective',{}).get('targets',[])
    recipes=craft_sources()['recipes']['recipes'];matches=[]
    if type(node.get('params',{}).get('current_crafting_recipe_id')) is int:
        return [r for r in recipes if r['id']==node['params']['current_crafting_recipe_id']]
    item=node.get('params',{}).get('item')
    if isinstance(item,dict) and type(item.get('item_id')) is int:
        # A native objective identity overrides display/preload candidates.
        domain=item.get('table_domain','ConsumableSheet')
        kinds={'ConsumableSheet':'item','EquipmentSheet':'equipment_id','QuestFakeItemSheet':'fakeitem_id'}
        if domain not in kinds:return []
        targets=[dict(kind=kinds[domain],table_data={'raw_id':item['item_id']})]
    for target in targets:
        args=target.get('arguments',[])
        if target.get('kind')=='equipment' and len(args)==1:
            matches += [r for r in recipes if r['equipment_output_symbol']==args[0]]
        elif target.get('kind')=='equipment_id':
            matches += [r for r in recipes if r['equipment_result_id']==target['table_data']['raw_id']]
        elif target.get('kind')=='item':
            rid=target.get('table_data',{}).get('raw_id')
            if type(rid) is int:matches += [r for r in recipes if any(i['raw_id']==rid for i in r['consumable_results'])]
        elif target.get('kind')=='fakeitem' and len(args)==1:
            matches += [r for r in recipes if any(i['fake_item_id']==args[0] for i in r['fake_item_results'])]
        elif target.get('kind')=='fakeitem_id':
            rid=target.get('table_data',{}).get('raw_id')
            if type(rid) is int:matches += [r for r in recipes if any(i['raw_id']==rid for i in r['fake_item_results'])]
    return list({r['id']:r for r in matches}.values())

def current_craft_program(node,snapshot):
    """Exact current typed crafting recipe, independent of catalog text hints."""
    from .arbeit_operations import _fresh,_scope
    from .quest_program import load_schema
    matches=[p for p in snapshot.get('objective_programs',[]) if _scope(p,node,objective=True)]
    if not matches:return None
    if len(matches)!=1:raise ValueError('ambiguous_current_crafting_program')
    row=matches[0];data=row.get('data') or {};schema=load_schema()
    spec=next(s for s in schema['classes'] if s['name']=='CraftingSuccessQuestObjectiveData')
    constant=next(g['constant'] for g in spec['getters'] if g['name']=='get_Type')
    if (row.get('confirmed') is not True or row.get('owner_verified') is not True or not _fresh(row,snapshot)
            or any(row.get(k) is None for k in ('quest_id','instance_id','accept_time','step_id','step_index','objective_index','process_started_at'))
            or any((row.get('source_version') or {}).get(k)!=schema[k] for k in ('metadata_sha256','game_assembly_sha256'))
            or data.get('type')!='MM.QDBL.CodeGenerated.CraftingSuccessQuestObjectiveData' or data.get('available') is not True
            or data.get('discriminant',{}).get('verified') is not True or data.get('discriminant',{}).get('value')!=constant):
        raise ValueError('current_crafting_program_scope_type_or_revision')
    owner=snapshot.get('current_quest') or {}
    if owner and any(row.get(k)!=owner.get(k) for k in ('quest_id','instance_id','accept_time','step_id','step_index','process_started_at')):
        raise ValueError('current_crafting_program_occurrence_changed')
    recipe=data.get('values',{}).get('CraftingRecipeName') or {}
    if (recipe.get('declared_type')!='Shared.Symbols.CraftingRecipeSheetId' or recipe.get('table_domain')!='CraftingRecipeSheet'
            or recipe.get('member')!='Id' or recipe.get('interpreted') is not True or recipe.get('domain_mapping_verified') is not True
            or type(recipe.get('value')) is not int):raise ValueError('current_crafting_recipe_domain_unverified')
    return dict(row,recipe_id=recipe['value']&0xffffffff)

def craft_request_node(node,snapshot):
    """Apply only the fresh native hold request for this objective occurrence."""
    if node.get('params',{}).get('material_requirement') is not None:
        from .material_supply import validate_material_request
        requests=[r for r in snapshot.get('material_hold_requests',[]) if r.get('node_id')==node['id']]
        if len(requests)!=1 or not validate_material_request(node,snapshot,requests[0]):raise ValueError('current_source_material_hold_request_required')
        bound=deepcopy(node);bound.setdefault('params',{})['item']=deepcopy(requests[0]['item'])
        return bound,requests[0]
    if node.get('op')=='life.craft':
        program=current_craft_program(node,snapshot)
        if program is None:return node,None
        bound=deepcopy(node);bound.setdefault('params',{})['current_crafting_recipe_id']=program['recipe_id']
        return bound,None
    if node.get('op')!='inventory.hold':return node,None
    from .arbeit_operations import _fresh,_scope
    requests=[r for r in snapshot.get('inventory_hold_requests',[]) if r.get('node_id')==node['id']]
    if not requests:return node,None
    if len(requests)!=1:raise ValueError('ambiguous_current_craft_hold_request')
    request=requests[0]
    if (request.get('ready') is not True or request.get('confirmed') is not True or not _fresh(request,snapshot)
            or not _scope(request,node,objective=True) or request.get('semantics')!='inventory.hold'):
        raise ValueError('current_craft_hold_request_scope')
    current=snapshot.get('current_quest') or {}
    if current and any(request.get(k)!=current.get(k) for k in ('quest_id','instance_id','accept_time','step_id','step_index','process_started_at')):
        raise ValueError('current_craft_hold_occurrence_changed')
    bound=deepcopy(node);bound.setdefault('params',{})['item']=deepcopy(request['item'])
    return bound,request

class CurrentCraftSession(QuestUIActionSession,CurrentProcessingSession):
    PANEL_NAMES=('CraftingHubPopupUIInstance','CraftingFacilityPopupUIInstance','CraftingPanelPopupUIInstance','CraftResultUIInstance')
    def declarations(self):
        base=super().declarations();own=craft_sources()['declarations']
        if base['metadata_sha256']!=own['metadata_sha256']:raise ValueError('craft_metadata_revision_changed')
        merged={}
        for row in base['types']+own['types']:
            prior=merged.get(row['name'])
            if prior and any(prior.get(k)!=row.get(k) for k in ('namespace','fields')):
                raise ValueError('conflicting_craft_type_declaration:'+row['name'])
            merged[row['name']]=row
        return dict(own,types=list(merged.values()))

    def source_shape(self,name,namespace=None):
        own=craft_sources()['result-shapes']
        rows=[s for s in own['classes'] if s['name']==name and (namespace is None or s['namespace']==namespace)]
        if rows:
            if len(rows)!=1 or any(own[k]!=self.plan[k] for k in ('metadata_sha256','game_assembly_sha256')):
                raise ValueError('craft_result_source_revision_or_type')
            return rows[0]
        return super().source_shape(name,namespace)

    def active_crafting_panels(self):
        r=self.reader;entry=self.schema('PresentationSingleton');klass=_class(r,self.base,entry);f=self.fields(klass,entry)
        instance=r.u64(r.u64(klass+0xb8)+f['instance'])
        if not instance or r.u64(instance)!=klass:raise MapReadPending('current_presentation_singleton')
        service=self.ref(instance,f,'<UIInstanceService>k__BackingField');f=self.object_fields(service,'UIInstanceService')
        table=self.ref(service,f,'handleTable');f=self.object_fields(table,'UIInstanceHandleTable');table=self.ref(table,f,'handleTable')
        wanted={t['name']:t['namespace'] for t in craft_sources()['declarations']['types'] if t['name'] in self.PANEL_NAMES};found={}
        for _,handle in self.dictionary_values(table,limit=self.plan['limits']['instances']):
            if not handle or r.class_name(r.u64(handle))!=('UIInstanceHandle',''):continue
            fields={f['name']:f for f in r.layout(r.u64(handle))['fields']};status=fields.get('<Status>k__BackingField',{});target=fields.get('<Instance>k__BackingField',{})
            if status.get('type_code') not in (8,17) or target.get('type_code') not in (18,21,28):raise ValueError('craft_ui_handle_type_changed')
            if struct.unpack('<i',self.watch(handle+status['offset'],4))[0]!=2:continue
            pointer=struct.unpack('<Q',self.watch(handle+target['offset'],8))[0]
            if not pointer:continue
            name,namespace=r.class_name(r.u64(pointer))
            if wanted.get(name)!=namespace:continue
            if name in found:raise MapReadPending('ambiguous_current_craft_panel:'+name)
            self.declared_fields(pointer,name);found[name]=pointer
        return found

    def selected_craft_id(self,pointer):
        name='CraftingPanelPopupUIInstance';fields=self.declared_fields(pointer,name);f=fields['recipeSheetId']
        own=craft_sources()['declarations']['types'];decl=next(t for t in own if t['name']==name);wrapper=next(t for t in own if t['name']=='CraftingRecipeSheetId')
        if f['type_code']!=17 or wrapper['flags']&0x18!=8 or wrapper['fields']!=[{'name':'Id','type_index':136776}]:raise ValueError('craft_recipe_wrapper_changed')
        index=[f['name'] for f in decl['fields']].index('recipeSheetId');table=self.reader.u64(self.reader.u64(pointer)+0x80)
        type_pointer=self.reader.u64(table+32*index+8)
        if type_definition_index(self, type_pointer)!=wrapper['definition_index']:raise MapReadPending('current_craft_recipe_runtime_definition')
        return struct.unpack('<i',self.watch(pointer+f['offset'],4))[0]&0xffffffff

    def crafting_model(self,handle):
        h=self.object_fields(handle,'MMMLUIHandle');shape=self.source_shape('XElem');klass=_class(self.reader,self.base,shape)
        if self.fields(klass,shape)!={'Forest':16,'Id':24}:raise ValueError('craft_xelem_shape_changed')
        address=handle+h['<ViewModelRoot>k__BackingField'];forest,element=struct.unpack('<Qi',self.watch(address,12))
        if not forest:raise MapReadPending('current_craft_model_root')
        names={'uiData','topCategoryGroup','currentButton','buttons','name','onclick','displayText','displayName',
            'quickCraftingTabData','skillCraftingTabData','items','slots','tables','craftingGroups','data','itemData','recipeSheetId',
            'isFacilityLevelValid','isSkillLevelValid','isDecorScoreInsufficient','isIngredientsOwned','resultData','itemIcon',
            'ownedAmount','rarity','requiredIngredients','inventoryCount','requiredCount','quantityData','spinnerData','value','isMin','isMax',
            'isFocused','onFocus','onUnfocus','onComplete','onChange','onClickLeftButton','onClickRightButton',
            'confirmButton','text','disabled','price','priceText','iconPath','createQuestButton','isFacilityLevelLow','facilityText',
            'isMoveToFacilityVisible','moveToFacilityButton','currentCategoryButtonIdx',
            'closeEvent','backButtonClick','canGoBack','closeButtonEvent'}
        allowed=set(craft_sources()['ui-source']['read_field_names'])|set(craft_sources()['declarations']['model_attributes'])
        if names-allowed:raise ValueError('craft_model_attribute_without_source:'+','.join(sorted(names-allowed)))
        view=XForestView(self,forest);queue=[element];seen=set();rows=[]
        while queue:
            ident=queue.pop(0)
            if ident in seen:continue
            if len(seen)>=4096:raise MapReadPending('craft_current_model_bound_exceeded')
            seen.add(ident);values=view.attributes(ident,names);children=view.children(ident)
            for value in values.values():
                if isinstance(value,dict) and 'element_id'in value:
                    if value['forest']!=forest:raise ValueError('craft_model_cross_forest')
                    queue.append(value['element_id']);value.pop('forest')
            rows.append(dict(element_id=ident,values=values,children=children));queue.extend(children)
        return dict(root_element_id=element,rows=rows)

    def reward_sheet_id(self,pointer,*path,integer_code=8):
        """Declared nested value fields, no assumed wrapper/stack-key offsets."""
        shape=self.source_shape('CommonRewardResult');self.typed_fields(pointer,shape['name']);address=pointer
        for index,member in enumerate(path):
            field=next(f for f in shape['fields'] if f['name']==member)
            offset=field['offset']-(16 if index else 0)
            if offset<0:raise ValueError('craft_reward_negative_inline_offset')
            address+=offset
            if index==len(path)-1:
                if integer_code not in (8,10) or field['type']['type_code']!=integer_code:raise ValueError('craft_reward_integer_type_changed')
                value=struct.unpack('<i' if integer_code==8 else '<q',self.watch(address,4 if integer_code==8 else 8))[0]
                return value&0xffffffff if integer_code==8 else value
            if field['type']['type_code']!=17:raise ValueError('craft_reward_path_not_inline')
            next_shape=self.source_shape(field['type']['name'],field['type']['namespace'])
            if next_shape['type_index']!=field['type']['definition_index'] or next_shape['value_type'] is not True:
                raise ValueError('craft_reward_nested_definition_changed')
            self.fields(_class(self.reader,self.base,next_shape),next_shape);shape=next_shape

    def craft_result(self,pointer):
        component=self.field(pointer,'CraftResultUIInstance','craftResultUIComponent')
        ui=self.field(pointer,'CraftResultUIInstance','titledScrollUI')
        if ui!=self.field(component,'CraftResultUIComponent','ui'):raise MapReadPending('craft_result_parent_changed')
        result=dict(ui_pointer=pointer,titled_ui_pointer=ui,
            is_active=self.field(component,'CraftResultUIComponent','isActive'),
            is_show_reserved=self.field(component,'CraftResultUIComponent','isShowReserved'),
            is_show_state=self.field(ui,'TitledScrollUI','isShowState'),
            parts_dirty=self.field(ui,'TitledScrollUI','isPartsDirty'),rewards={},button_controls=[],
            source='current CraftResultUIInstance -> component -> same TitledScrollUI and CommonRewardResult collections',
            missing=['current native result button screen transform/active hierarchy/raycast clearance'])
        types=self.source_shape('CommonRewardResultType')['enum_constants']
        lookup={value:key for key,value in types.items()}
        for group in ('successRewards','criticalSuccessRewards','additionalRewards','failedRewards','expiredRewards'):
            collection=self.field(component,'CraftResultUIComponent',group);rows=[]
            for reward in self.refs(collection,element='CommonRewardResult',limit=256) if collection else []:
                kind=self.field(reward,'CommonRewardResult','Type');amount=self.field(reward,'CommonRewardResult','<Amount>k__BackingField')
                if kind not in lookup or amount<0:raise ValueError('craft_reward_type_or_amount')
                row=dict(type=lookup[kind],amount=amount,rarity=self.field(reward,'CommonRewardResult','Rarity'),item_id=None,
                    identity_source='current CommonRewardResult.'+lookup[kind],collection=group)
                if kind==types['Consumable']:
                    row.update(item_id=self.reward_sheet_id(reward,'ConsumableStackKey','SheetId','Id'),table_domain='ConsumableSheet')
                elif kind in (types['UnidentifiedEquipment'],types['CarvedUnidentifiedEquipment']):
                    row.update(item_id=self.reward_sheet_id(reward,'UnidentifiedEquipmentStackKey','sheetId','Id'),table_domain='EquipmentSheet')
                elif kind==types['QuestFakeItem']:
                    row.update(item_id=self.reward_sheet_id(reward,'QuestFakeItemSheetId','Id'),table_domain='QuestFakeItemSheet')
                elif kind==types['Equipment']:
                    row['non_stackable_id']=self.reward_sheet_id(reward,'NonStackableId','rawValue',integer_code=10)
                    row['missing']='regular Equipment reward NonStackableId -> current equipment instance -> sheet/season join'
                rows.append(row)
            result['rewards'][group]=rows
        collection=self.field(ui,'TitledScrollUI','partsControls')
        for control in self.refs(collection,element='ITitledScrollUIPartsControl',limit=64):
            if self.reader.class_name(self.reader.u64(control))!=('TitledScrollUICommonButtonPartsControl','MM.Client.Presentation.UI.Controls.TitledScrollUI'):continue
            state=dict(control_pointer=control,shown=self.field(control,'TitledScrollUICommonButtonPartsControl','isShowState'))
            for member in ('confirmButton','otherButton','toggleButton'):
                button=self.field(control,'TitledScrollUICommonButtonPartsControl',member)
                state[member]=dict(present=bool(button),control_pointer=button)
                if button:state[member].update(enabled=self.field(button,'ButtonControl','enabled'),interactable=self.field(button,'ButtonControl','interactable'),
                    root_object=self.field(button,'ButtonControl','<RootObject>k__BackingField'),
                    callback_pointer=self.field(button,'ButtonControl','ButtonActionCallback'))
            result['button_controls'].append(state)
        return result

    def current_crafting(self):
        self._guards=[];sources=craft_sources();client=Path(self.reader.image).parent
        table_revision=verify_processing_sheet_revision(client,sources['recipes']['client_revision_checks'])
        from mini_masukja.mmml_source import verify_mmml_source
        source_proof=verify_mmml_source(client,sources['ui-source']['source_sha256'])
        panels={}
        for name,pointer in self.active_crafting_panels().items():
            f=self.declared_fields(pointer,name)
            if name=='CraftResultUIInstance':
                panels[name]=self.craft_result(pointer)
                continue
            handle=self.declared_ref(pointer,f,'handle');row=dict(ui_pointer=hex(pointer),ui_type=name,handle_pointer=handle,layout=self.processing_layout(handle),model=self.crafting_model(handle))
            if name=='CraftingPanelPopupUIInstance':
                row['recipe_id']=self.selected_craft_id(pointer)
                for key in ('minProductCount','maxProductCount','initalProductCount'):row[key]=self.declared_scalar(pointer,f,key,codes=(8,))
            panels[name]=row
        if any(self.reader.read(a,len(raw))!=raw for a,raw in self._guards):raise MapReadPending('craft_ui_changed_during_read')
        return dict(stable=True,panels=panels,source='current UIInstanceService -> craft instance -> MMML handle/model/layout',
            observed_at=datetime.now(timezone.utc).isoformat(),metadata_sha256=sources['declarations']['metadata_sha256'],
            table_revision_sha256=table_revision,mmml_sha256=sources['ui-source']['source_sha256'],
            current_mmml_sha256=source_proof['decoded_sha256'],mmml_source_proof=source_proof,process_writes=0,game_inputs=0)

    def current_result_pointer_scene(self,probe_context,frame):
        """Guard reread of the same active result root/control, without input."""
        current=self.current_crafting();result=current['panels'].get('CraftResultUIInstance')
        if not result:raise MapReadPending('owned crafting result no longer active')
        if result['ui_pointer']!=probe_context.get('target_ui_pointer'):
            raise MapReadPending('crafting result root changed before pointer click')
        candidate=craft_result_target(self,dict(result=result),frame)
        for name in ('control_pointer','root_object_pointer','model_event_id','window_id'):
            if candidate.get(name)!=probe_context.get(name):raise MapReadPending('crafting result target changed: '+name)
        self.assert_stable()
        snapshot=dict(pointer_state=candidate['pointer_state'],ui_pointer=result['ui_pointer'],window_id=result['ui_pointer'],
            target_id='craft.result.confirm',ui_type='CraftResultUIInstance',
            pointer_semantics=deepcopy(result.get('rewards',{})),
            source='current CraftResultUIInstance same-parent confirm ButtonControl',
            source_hash=craft_sources()['result-shapes']['game_assembly_sha256'],captured_at=current['observed_at'])
        return candidate,snapshot

class Model:
    def __init__(self,raw):
        self.rows={r['element_id']:r for r in raw['rows']};self.root=raw['root_element_id']
        if len(self.rows)!=len(raw['rows']):raise ValueError('duplicate_craft_model_element')
    def values(self,ident):return self.rows[ident]['values']
    def ref(self,ident,name):
        value=self.values(ident).get(name)
        return value.get('element_id') if isinstance(value,dict) else None
    def path(self,ident,*names):
        for name in names:
            ident=self.ref(ident,name)
            if ident is None:return None
        return ident
    def children(self,ident):return self.rows[ident]['children'] if ident is not None else []
    def list(self,ident,name):return self.children(self.ref(ident,name))

def _event(row,key='onclick'):return isinstance(row.get(key),dict) and row[key].get('event_present') is True

def material_requirements(scene,recipe):
    """Source IDs joined to the same current detail; counts retain UI semantics."""
    if scene.get('stage')!='detail' or scene.get('selected_recipe_id')!=recipe['id']:return [],['current selected recipe material parent']
    count=(scene.get('quantity') or {}).get('value');materials=scene.get('materials') or []
    if type(count) is not int or count<=0:return [],['current positive crafting batch count']
    expected=[dict(r,table_domain='ConsumableSheet') for r in recipe['consumable_ingredients']]+[
        dict(r,name=r['display_name'],table_domain='QuestFakeItemSheet') for r in recipe['fake_item_ingredients']]
    if len(materials)!=len(expected):return [],['current material/source row count mismatch']
    rows=[];used=set()
    for need in expected:
        matches=[(i,r) for i,r in enumerate(materials) if r.get('item',{}).get('displayName')==need['name']]
        if len(matches)!=1 or matches[0][0] in used:return [],['current material/source unique identity']
        i,row=matches[0];used.add(i);have=row.get('inventoryCount');required=row.get('requiredCount')
        if type(have) is not int or have<0 or type(required) is not int or required!=need['amount']*count:
            return [],['current material/source amount mismatch']
        rows.append(dict(item_id=need['raw_id'],table_domain=need['table_domain'],name=need['name'],
            per_batch_quantity=need['amount'],batches=count,required_quantity=required,observed_quantity=have,
            shortage=max(0,required-have),recipe_id=recipe['id'],source_ref=need.get('source_ref',recipe['source_ref']),
            identity_binding='selected native recipe + exact unique current material display and amount',
            quantity_basis='current requiredIngredients.inventoryCount/requiredCount; protected stock interpretation not inferred'))
    return rows,[]

def supply_eligibility(scene,recipe,hold_request=None):
    """Current detail eligibility only; missing facts never mean eligible."""
    if scene.get('stage')!='detail' or scene.get('selected_recipe_id')!=recipe['id']:return None
    materials,missing=material_requirements(scene,recipe);confirm=scene.get('confirm') or {}
    constrained=hold_request and any(c.get('has_value') is True for c in (hold_request.get('constraints') or {}).values())
    false=(confirm.get('disabled') is True or scene.get('facility_level_low') is True or scene.get('decor_score_insufficient') is True
        or bool(materials and any(r['shortage']>0 for r in materials)))
    true=(not missing and not constrained and confirm.get('disabled') is False and _event(confirm)
        and scene.get('facility_level_low') is False and scene.get('decor_score_insufficient') is False and 'price' in confirm)
    if not false and not true:return None
    return dict(kind='native_craft',route_id='crafting:'+str(recipe['id']),recipe_id=recipe['id'],eligible=False if false else True,
        identity_binding='current native selected recipe and same-parent confirm/material/facility data',
        basis='current selected crafting detail eligibility; ownership/goal/price still checked before input',
        material_shortage=any(r['shortage']>0 for r in materials),source_refs=scene.get('source_refs',[]))

def _rect_target(layout,candidate,role,viewport):
    scale=layout['scale'];rect=[x*scale for x in candidate['rect_ui']];x,y,w,h=rect
    if min(w,h)<=0 or x<0 or y<0 or x+w>viewport[0] or y+h>viewport[1]:return None
    return dict(kind='click',role=role,point=[round(x+w/2,3),round(y+h/2,3)],source_rect=[x,y,x+w,y+h],
        layout_index=candidate['index'],window_id=layout['window_id'],source='current MMML output + source ordered model')

def craft_recipe_scroll(scene,viewport,texts,occlusion_reader=None):
    """One current wheel notch toward an offscreen source-indexed recipe.

    MMML895 and 901 have different mask rules from processing's MMML911.
    Actual rendered captions within the native scroll/row intersection establish
    visible rows; no processing inset, saved offset or inferred pixel delta is used.
    """
    listing=scene.get('recipe_list') or {};layout=listing.get('layout') or {};target=listing.get('target_index')
    if type(target) is not int:return None
    scale=layout.get('scale');captions=listing.get('captions') or {};tags=listing.get('row_tags',[])
    if not scale or not captions:return None
    parents=[]
    for parent in layout.get('candidates',[]):
        if parent.get('tag')!='vscroll':continue
        x,y,w,h=[v*scale for v in parent['rect_ui']];clip=[max(0,x),max(0,y),min(viewport[0],x+w),min(viewport[1],y+h)]
        visible=[]
        for row in layout['candidates']:
            name=row.get('name','')
            if row.get('tag') not in tags or not name.isdecimal() or int(name) not in captions:continue
            rx,ry,rw,rh=[v*scale for v in row['rect_ui']]
            left,top,right,bottom=max(clip[0],rx),max(clip[1],ry),min(clip[2],rx+rw),min(clip[3],ry+rh)
            labels=[t for t in texts if t.get('score',0)>=.85 and t.get('text')==captions[int(name)] and len(t.get('box',[]))==4
                and left<=t['box'][0]<=t['box'][2]<=right and top<=t['box'][1]<=t['box'][3]<=bottom]
            if len(labels)==1 and right>left and bottom>top:
                visible.append(dict(index=int(name),rect_ui=row['rect_ui'],source_intersection=[left,top,right-left,bottom-top]))
        if visible:parents.append((parent,visible))
    if len(parents)!=1:return None
    parent,visible=parents[0];indices=[r['index'] for r in visible]
    selected=visible[len(visible)//2];x,y,w,h=selected['source_intersection'];point=[round(x+w/2,3),round(y+h/2,3)]
    signature=hashlib.sha256(json.dumps(visible,sort_keys=True).encode()).hexdigest()
    observation=dict(window_id=layout['window_id'],digest=signature,visible_indices=indices,rows=visible,
        source_function=listing['source_function'],basis='current native vscroll + source indexed recipe rows + current rendered caption containment')
    scene['scroll_observation']=observation
    if scene.get('actions',{}).get('recipe'):return None
    if target<min(indices):delta=120
    elif target>max(indices):delta=-120
    else:return None
    candidate=dict(parent,rect_ui=[x/scale,y/scale,w/scale,h/scale])
    proof=occlusion_reader(window_id=layout['window_id'],rect_ui=candidate['rect_ui']) if occlusion_reader else None
    if (not isinstance(proof,dict) or proof.get('occluded') is not False or proof.get('basis')!='current exact target raycast result'
            or proof.get('window_id')!=layout['window_id'] or proof.get('rect_ui')!=candidate['rect_ui']):
        scene.setdefault('missing',[]).append('current crafting scroll target raycast ownership');return None
    action=dict(kind='scroll',role='craft.recipe.scroll',point=point,delta=delta,source_rect=[x,y,x+w,y+h],
        safe_rect=[x,y,w,h],layout_index=parent['index'],window_id=layout['window_id'],source='current source-indexed crafting recipe scroll',occlusion_observation=proof)
    scene['actions']['scroll']=action
    return action

def craft_scroll_captions(scene,image,ocr):
    """Observe only current source row/vscroll intersections at scroll boundaries."""
    listing=scene.get('recipe_list') or {};layout=listing.get('layout') or {}
    candidates=layout.get('candidates',[]);scale=layout.get('scale');regions=[]
    if type(scale) not in (int,float) or not math.isfinite(scale) or scale<=0:return []
    width,height=image.size
    for parent in (r for r in candidates if r.get('tag')=='vscroll'):
        px,py,pw,ph=[v*scale for v in parent['rect_ui']]
        for row in candidates:
            if row.get('tag') not in listing.get('row_tags',[]) or not str(row.get('name','')).isdigit():continue
            if int(row['name']) not in listing.get('captions',{}):continue
            x,y,w,h=[v*scale for v in row['rect_ui']]
            box=(math.ceil(max(0,x,px)),math.ceil(max(0,y,py)),math.floor(min(width,x+w,px+pw)),math.floor(min(height,y+h,py+ph)))
            if box[2]>box[0] and box[3]>box[1] and box not in regions:regions.append(box)
    # No scene-wide fallback. The current source list bounds this work.
    texts=[]
    for left,top,right,bottom in regions:
        for row in ocr.read(image.crop((left,top,right,bottom))):
            copy=deepcopy(row);x1,y1,x2,y2=copy['box'];copy['box']=[x1+left,y1+top,x2+left,y2+top];texts.append(copy)
    scene['scroll_caption_regions']=[list(r) for r in regions]
    return texts

def craft_result_target(session,scene,frame):
    """Same-parent native confirm control; its hit proof is a separate step."""
    result=scene.get('result') or {}
    if (result.get('is_active') is not True or result.get('is_show_state') is not True
            or result.get('is_show_reserved') is not False or result.get('parts_dirty') is not False):
        raise MapReadPending('current crafting result presentation is not stable')
    shown=[r for r in result.get('button_controls',[]) if r.get('shown') is True]
    if len(shown)!=1:raise MapReadPending('unique current crafting result confirm control required')
    button=shown[0].get('confirmButton') or {}
    if (button.get('present') is not True or button.get('enabled') is not True or button.get('interactable') is not True
            or not all(button.get(k) for k in ('control_pointer','root_object','callback_pointer'))):
        raise MapReadPending('current crafting result confirm callback and enabled root required')
    geometry=session.read_control_geometry(button['control_pointer'],result['ui_pointer'],frame)
    if (geometry.get('ui_pointer')!=result['ui_pointer'] or geometry.get('control_pointer')!=button['control_pointer']
            or geometry.get('root_object_pointer')!=button['root_object']):
        raise MapReadPending('current crafting result native control identity changed')
    geometry.update(index=0,window_id=result['ui_pointer'],window_kind='native-ui-root',
        model_event_id=button['callback_pointer'],event_kind='native-button-delegate',button_pointer=button['control_pointer'],
        pointer_state=session.current_pointer_state())
    scene['result_target']=geometry
    scene['result_safe_rect']=deepcopy((frame.get('current_context') or {}).get('safe_rect'))
    return geometry

def craft_return_ui(current,recipe,scene,snapshot,frame):
    """Own craft close callbacks; result acknowledgement is a separate surface."""
    sources=craft_sources();owner=snapshot.get('input_owner') or {}
    result=dict(source='current crafting model close callback + pinned crafting MMML',source_family='native_craft',
        observed_at=scene['observed_at'],session_id=snapshot.get('session_id'),character_id=snapshot.get('character_id'),
        process_started_at=snapshot.get('process_started_at') or snapshot.get('window_preflight',{}).get('selected',{}).get('process_started_at'),
        source_revision=current.get('mmml_sha256'),stable=current.get('stable') is True,confirmed=False,
        recipe_id=recipe['id'],input_epoch=owner.get('epoch'),owner_proof=deepcopy(owner),key_receiver_confirmed=False,
        source_refs=scene['source_refs'],missing=[])
    if current.get('stable') is not True or current.get('mmml_sha256')!=sources['ui-source']['source_sha256']:
        result['missing']=['current crafting return source revision'];return result
    panels=current.get('panels') or {}
    if 'CraftResultUIInstance' in panels:result['missing']=['craft result requires acknowledgement; Escape is not substituted'];return result
    choices=[('detail','CraftingPanelPopupUIInstance',918,'closeEvent'),('facility','CraftingFacilityPopupUIInstance',927,'backButtonClick'),('hub','CraftingHubPopupUIInstance',937,'closeButtonEvent')]
    selection=next((r for r in choices if r[1] in panels),None)
    if selection is None:
        context=frame.get('current_context') or snapshot.get('current_context') or {}
        if not panels and context.get('ok') is True and context.get('field_hud') is True:
            result.update(surface='hud',confirmed=True,panels_absent_confirmed=True,active_owned_panels=[])
        else:result['missing']=['current typed craft return panels and field HUD']
        return result
    surface,name,function,callback=selection;panel=panels[name];model=Model(panel['model']);values=model.values(model.root)
    result.update(surface=surface,panel_id=panel.get('ui_pointer'),window_id=panel['layout']['window_id'],
        ui_pointer=panel.get('ui_pointer'),current_layout=deepcopy(panel['layout']),frame=deepcopy(frame))
    if surface=='detail' and panel.get('recipe_id')!=recipe['id']:result['missing']=['current craft return recipe identity mismatch'];return result
    if surface=='facility':
        if type(values.get('canGoBack')) is not bool:result['missing']=['current craft facility canGoBack'];return result
        result['can_go_back']=values['canGoBack'];control='backEvent' if values['canGoBack'] else 'closeEvent'
    else:control='onclose' if surface=='detail' else 'closeEvent'
    event=values.get(callback)
    if not isinstance(event,dict) or event.get('event_present') is not True:result['missing']=['current craft return callback'];return result
    source=next(f for f in sources['ui-source']['functions'] if f['index']==function)
    binding=dict(function=function,code_sha256=source['code_sha256'],callback=callback,control=control)
    result.update(confirmed=True,close=dict(callback=deepcopy(event),callback_event=deepcopy(event),callback_name=callback,
        control=control,enabled=True,closable=True if surface=='detail' else None,key='ESC',static_source_pinned=binding,
        source_binding=binding,source_refs=scene['source_refs'],basis='same-parent current callback; exact Escape receiver remains separate'))
    return result

def scene_from_crafting(current,recipe,viewport):
    """Project same-parent model facts; no model list is replaced by source rows."""
    out=dict(stage='unavailable',recipe_id=recipe['id'],actions={},missing=[],typed_current=current,materials=[],output=None)
    if current.get('stable') is not True:raise ValueError('craft_current_not_stable')
    sources=craft_sources()
    if current.get('metadata_sha256')!=sources['declarations']['metadata_sha256'] or current.get('mmml_sha256')!=sources['ui-source']['source_sha256']:
        raise ValueError('craft_current_source_revision')
    panels=current.get('panels',{})
    if 'CraftResultUIInstance'in panels:
        result=panels['CraftResultUIInstance']
        return dict(out,stage='result',result=result,missing=result.get('missing') or ['current native result button screen transform/active hierarchy/raycast clearance'])
    selected=next((k for k in ('CraftingPanelPopupUIInstance','CraftingFacilityPopupUIInstance','CraftingHubPopupUIInstance') if k in panels),None)
    if selected is None:return out
    panel=panels[selected];layout=panel['layout'];scale=layout['scale']
    if any(abs(a*scale-b)>2 for a,b in zip(layout['screen_ui'],viewport)):raise ValueError('craft_current_viewport_mismatch')
    model=Model(panel['model']);data=model.ref(model.root,'uiData');data=model.root if data is None else data;candidates=layout['candidates']
    def target(rows,role):
        return _rect_target(layout,rows[0],role,viewport) if len(rows)==1 else None
    if selected=='CraftingPanelPopupUIInstance':
        out.update(stage='detail',selected_recipe_id=panel.get('recipe_id'))
        if panel.get('recipe_id')!=recipe['id']:out['missing'].append('selected native recipe ID differs');return out
        values=model.values(data);confirm=model.ref(data,'confirmButton');quantity=model.path(data,'quantityData','spinnerData')
        if confirm is not None:out['confirm']=deepcopy(model.values(confirm))
        if quantity is not None:out['quantity']=deepcopy(model.values(quantity))
        result=model.ref(data,'resultData')
        if result is not None:
            out['output']=deepcopy(model.values(result));icon=model.ref(result,'itemIcon')
            if icon is not None:out['output']['item_icon']=deepcopy(model.values(icon))
        for ident in model.list(data,'requiredIngredients'):
            item=deepcopy(model.values(ident));icon=model.ref(ident,'itemData')
            if icon is not None:item['item']=deepcopy(model.values(icon))
            out['materials'].append(item)
        out['facility_level_low']=values.get('isFacilityLevelLow');out['decor_score_insufficient']=values.get('isDecorScoreInsufficient')
        # In source 886 the confirm CTA is the final CTA instantiation, after
        # upgrade/create-quest controls; 163 emits Button.Base.Tooltip once.
        ctas=[c for c in candidates if c['tag']=='Button.Base.Tooltip']
        if ctas:
            last=max(ctas,key=lambda c:c['index']);out['actions']['submit']=_rect_target(layout,last,'craft.submit',viewport)
        inputs=[c for c in candidates if c['tag']=='TextInput.IntegerNumber']
        out['actions']['quantity']=target(inputs,'craft.quantity')
        out['material_requirements'],out['material_missing']=material_requirements(out,recipe)
        return out
    if selected=='CraftingHubPopupUIInstance':
        out['stage']='hub';group=model.ref(data,'topCategoryGroup')
        if group is None:out['missing'].append('current top category group');return out
        buttons=model.list(group,'buttons');current_tab=model.values(group).get('currentButton');wanted='QuickCrafting' if recipe['is_basic_crafting'] else 'SkillCrafting';out['tab']=current_tab
        if current_tab!=wanted:
            indexes=[i for i,e in enumerate(buttons) if model.values(e).get('name')==wanted and _event(model.values(e))]
            bars=[c for c in candidates if c['tag']=='t-MainTabButtonBar']
            if len(bars)==1:
                bx,by,bw,bh=bars[0]['rect_ui'];rows=sorted([c for c in candidates if c['tag']=='Button.Base' and bx<=c['rect_ui'][0] and by<=c['rect_ui'][1] and c['rect_ui'][0]+c['rect_ui'][2]<=bx+bw and c['rect_ui'][1]+c['rect_ui'][3]<=by+bh],key=lambda c:c['index'])
                if len(indexes)==1 and len(rows)==len(buttons):out['actions']['tab']=_rect_target(layout,rows[indexes[0]],'craft.tab.'+wanted,viewport)
            out['missing'].append('current tab ordered rectangle binding' if not out['actions'].get('tab') else '');return out
        if wanted=='QuickCrafting':
            parent=model.ref(data,'quickCraftingTabData');items=model.list(parent,'items') if parent is not None else []
            matches=[i for i,e in enumerate(items) if (model.values(e).get('recipeSheetId',-1)&0xffffffff)==recipe['id'] and _event(model.values(e))]
            if len(matches)==1:
                rows=[c for c in candidates if c['tag']=='t-QuickCraftingItem' and c['name']==str(matches[0])]
                out['actions']['recipe']=target(rows,'craft.recipe.'+str(recipe['id']))
                captions={}
                for i,ident in enumerate(items):
                    icon=model.ref(ident,'itemData')
                    if icon is not None and isinstance(model.values(icon).get('displayName'),str):captions[i]=model.values(icon)['displayName']
                out['recipe_list']=dict(layout=layout,target_index=matches[0],captions=captions,row_tags=['t-QuickCraftingItem'],source_function=933)
            return out
        parent=model.ref(data,'skillCraftingTabData');slots=model.list(parent,'slots') if parent is not None else []
        category_ids={s['parent_category_id'] for s in sources['recipes']['subcategories'] if s['facility_type']==recipe['facility_type'] and s['recipe_display_group_type']==recipe['crafting_recipe_display_group_type']}
        titles={c['title'] for c in sources['recipes']['categories'] if c['id'] in category_ids}
        matches=[i for i,e in enumerate(slots) if model.values(e).get('displayName') in titles and _event(model.values(e))]
        rows=sorted([c for c in candidates if c['tag']=='Button.Base.Big'],key=lambda c:c['index'])
        if len(matches)==1 and len(rows)==len(slots):out['actions']['category']=_rect_target(layout,rows[matches[0]],'craft.category.'+str(recipe['id']),viewport)
        else:out['missing'].append('source category/current ordered slot join absent or ambiguous')
        return out
    out['stage']='facility';items=[]
    for table in model.list(data,'tables'):
        for group in model.list(table,'craftingGroups'):items.extend(model.list(group,'items'))
    expected={recipe['display_name'],recipe.get('equipment_output_name')}|{i['name'] for i in recipe['consumable_results']}|{i['display_name'] for i in recipe['fake_item_results']};expected.discard('');expected.discard(None)
    matches=[]
    for i,ident in enumerate(items):
        icon=model.ref(ident,'itemData')
        if icon is not None and model.values(icon).get('displayName') in expected and _event(model.values(ident)):matches.append(i)
    if len(matches)==1:
        rows=[c for c in candidates if c['tag']=='Button.Base.Big' and c['name']==str(matches[0])]
        out['actions']['recipe']=target(rows,'craft.recipe.'+str(recipe['id']))
        out['selection_binding']='current unique display candidate; native selected recipe ID required before any craft'
        captions={}
        for i,ident in enumerate(items):
            icon=model.ref(ident,'itemData')
            if icon is not None and isinstance(model.values(icon).get('displayName'),str):captions[i]=model.values(icon)['displayName']
        out['recipe_list']=dict(layout=layout,target_index=matches[0],captions=captions,row_tags=['__placeholder'],source_function=927)
    else:out['missing'].append('current recipe item display candidate absent or ambiguous')
    return out

class CraftObservationProvider:
    def __init__(self,*,allow_process_read=False,session_factory=CurrentCraftSession,recipe_selector=None,ocr=None,occlusion_reader=None):
        self.allow_process_read=allow_process_read;self.session_factory=session_factory;self.recipe_selector=recipe_selector
        self.ocr=ocr;self.occlusion_reader=occlusion_reader
        self._result_history={}
    def __call__(self,*,window,node,image,frame,snapshot,session=None):
        result=dict(ok=False,missing=[],source='source crafting observer',game_inputs=0,live_verified=False)
        try:
            if not all(snapshot.get(k) and frame.get(k)==snapshot[k] for k in ('session_id','character_id')):raise ValueError('craft_current_frame_owner')
            if not frame.get('captured_at') or not frame.get('window_signature'):raise ValueError('craft_current_frame_identity')
            rect=tuple(window.client_rect);viewport=(rect[2]-rect[0],rect[3]-rect[1])
            if tuple(image.size)!=viewport:raise ValueError('craft_frame_size')
            saved=(snapshot.get('pending_input') or {}).get('planner_receipt') or {}
            owned_recipe=(saved.get('child_receipt') or {}).get('recipe_id')
            if (saved.get('phase') in ('craft.submit','craft.result.probe','craft.result.ack','craft.supply_cleanup') and saved.get('node_id')==node['id']
                    and all(saved.get(k)==snapshot.get(k) for k in ('session_id','character_id'))):
                # Cleanup remains bound to the submitted recipe after a native
                # step transition. This does not authorize another production.
                candidates=[r for r in craft_sources()['recipes']['recipes'] if r['id']==owned_recipe];hold_request=None
            else:
                bound,hold_request=craft_request_node(node,snapshot);candidates=recipe_candidates(bound)
            selection=snapshot.get('supply_selection') or {};candidate=selection.get('candidate') or {}
            if self.recipe_selector:selected=self.recipe_selector(node,snapshot,candidates)
            elif candidate.get('kind')=='native_craft' and selection.get('status') in ('inspect','owned','eligible'):
                selected=candidate.get('recipe_id')
                if type(selected) is not int or candidate.get('route_id')!='crafting:'+str(selected):raise ValueError('craft_supply_candidate_identity')
            else:selected=candidates[0]['id'] if len(candidates)==1 else None
            matches=[r for r in candidates if r['id']==selected]
            if len(matches)!=1:raise ValueError('unique_or_explicit_source_crafting_recipe')
            recipe=matches[0];context=frame.get('current_context') or snapshot.get('current_context') or {}
            pending=snapshot.get('pending_input');scene=None
            def decorate(scene):
                scene.update(source='source crafting current-parent supplier',observed_at=frame['captured_at'],confirmed=True,
                    session_id=snapshot['session_id'],character_id=snapshot['character_id'],viewport=list(viewport),window_signature=frame['window_signature'],
                    input_owner=deepcopy(snapshot.get('input_owner')),recipe_id=recipe['id'],source_refs=[recipe['source_ref'],'data/arbeit-craft/ui-source.json'],
                    current_binding_kind='typed current native recipe' if scene['stage']=='detail' else 'source selection candidate')
                return scene
            if not pending and not snapshot.get('supply_ui_cleanup_required') and context.get('ok') is True and context.get('field_hud') is True and context.get('profile',{}).get('visual_confirmed') is True:
                scene=dict(stage='field_hud',recipe_id=recipe['id'],actions={'open':{'kind':'key','key':'K','role':'craft.open'}},missing=[])
            else:
                if not self.allow_process_read:raise ValueError('read_only_craft_process_access_disabled')
                def read(opened):
                    current=opened.current_crafting();scene=scene_from_crafting(current,recipe,viewport)
                    decorate(scene)
                    if scene.get('recipe_list') and (not scene['actions'].get('recipe') or ((pending or {}).get('planner_receipt') or {}).get('phase') in ('craft.scroll','craft.scroll.probe')):
                        from .recipe_scroll import attach_craft_scroll
                        try:attach_craft_scroll(opened,scene,current,node,snapshot,frame)
                        except (OSError,RuntimeError,ValueError,KeyError,TypeError) as exc:
                            scene['missing'].append('current native recipe scroll: '+str(exc))
                    if scene['stage']=='result':
                        from .objective_inventory import bind_crafting_equipment_rewards
                        # Keep raw rewards unchanged: the pointer guard rereads
                        # these exact native UI values before ACK. Inventory
                        # joins are a separate current-instance evidence lane.
                        scene['equipment_reward_bindings']=bind_crafting_equipment_rewards(scene['result'],snapshot)
                        scene['result_observed_at']=current['observed_at']
                        try:
                            candidate=craft_result_target(opened,scene,frame)
                            from .quest_ui_actions import prepare_pointer_proposal
                            ui_snapshot=dict(pointer_state=candidate['pointer_state'],ui_pointer=scene['result']['ui_pointer'],window_id=scene['result']['ui_pointer'],
                                target_id='craft.result.confirm',ui_type='CraftResultUIInstance',
                                pointer_semantics=deepcopy(scene['result'].get('rewards',{})),
                                source='current CraftResultUIInstance same-parent confirm ButtonControl',source_hash=craft_sources()['result-shapes']['game_assembly_sha256'],
                                captured_at=current['observed_at'])
                            pointer_frame=dict(frame,input_owner=deepcopy(snapshot.get('input_owner')),pending_input=deepcopy(pending))
                            if snapshot.get('pointer_probe_receipt'):pointer_frame['pointer_probe_receipt']=deepcopy(snapshot['pointer_probe_receipt'])
                            scene['result_pointer']=prepare_pointer_proposal(candidate,ui_snapshot,pointer_frame)
                        except (ImportError,OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
                            scene['missing'].append('current craft result target: '+str(exc))
                    return_ui=craft_return_ui(current,recipe,scene,snapshot,frame)
                    from .material_close import attach_material_close_geometry
                    try:attach_material_close_geometry(opened,return_ui,frame)
                    except (ImportError,AttributeError) as exc:return_ui.setdefault('close_missing',[]).append(str(exc))
                    opened.assert_stable()
                    return current,scene,return_ui
                if session is not None:current,scene,return_ui=read(session)
                else:
                    with self.session_factory(window.pid) as opened:current,scene,return_ui=read(opened)
                result['typed_current']=current
                result['material_return_ui']=return_ui
            decorate(scene)
            scene['hold_request']=deepcopy(hold_request)
            observed={k:scene[k] for k in ('source','observed_at','session_id','character_id')}
            if scene.get('scroll_observation'):scene['scroll_observation'].update(observed)
            for row in scene.get('material_requirements',[]):row.update(observed,node_id=node['id'],confirmed=True)
            eligible=supply_eligibility(scene,recipe,hold_request)
            result['supply_evidence']=[dict(eligible,**observed,node_id=node['id'])] if eligible is not None else []
            saved=(pending or {}).get('planner_receipt') or {};child=saved.get('child_receipt') or {}
            operation_id=(pending or {}).get('operation_id');owner=snapshot.get('input_owner') or {}
            history_key=(operation_id,node['id'],recipe['id'],owner.get('epoch'),scene['session_id'],scene['character_id'],scene['window_signature'])
            self._result_history={k:v for k,v in self._result_history.items() if k==history_key}
            if scene['stage']=='result':self._result_history[history_key]=dict(observed,ui_pointer=scene['result'].get('ui_pointer'))
            typed=result.get('typed_current') or {};active=list((typed.get('panels') or {}).keys())
            if ((saved.get('phase') or '').startswith('craft.') and saved.get('node_id')==node['id'] and child.get('recipe_id')==recipe['id']
                    and operation_id and typed.get('stable') is True and not active
                    and context.get('ok') is True and context.get('field_hud') is True and context.get('profile',{}).get('visual_confirmed') is True):
                proof=dict(observed,node_id=node['id'],recipe_id=recipe['id'],operation_id=operation_id,window_signature=scene['window_signature'],
                    input_owner=deepcopy(owner),confirmed=True,active_crafting_ui_types=[],result_ui_present=False,field_hud=True,
                    basis='current typed active UIInstanceHandleTable enumeration + current confirmed field HUD',
                    source_refs=scene['source_refs'],prior_result_observation=self._result_history.get(history_key),
                    saved_result_observations=deepcopy(child.get('result_observations',[])))
                result['result_cleanup_evidence']=[proof]
                scene['stage']='field_hud';scene['actions']={}
            result.update(ok=True,crafting=scene);return result
        except (OSError,ValueError,RuntimeError,KeyError,TypeError,AttributeError) as exc:result['missing'].append(str(exc));return result
