
    i`+                        U d Z ddlmZ ddlZddlZddlZddlmZmZ ddl	m
Z
  ej                  e      Zg dZded<    ed	d
h      Zded<    eh d      Zded<   e G d d             Z G d d      Zy)u  
core/merge/patch_reconciler.py

PatchReconciler — validates and applies an Opus-resolved patch before it is
committed to the master state.

Validation pipeline (all steps run, errors accumulated before returning):
    Step 1: Schema check  — patch is a list of dicts; each entry has "op" and
                            "path" keys.
    Step 2: Dry-run apply — apply patch to state.copy() using a simple RFC 6902
                            engine (add / replace / remove on nested paths).
                            The original state is NEVER mutated.
    Step 3: Axiom checks  — scan the resulting new_state for forbidden patterns
                            such as "sqlite3", "API_KEY", and "sk-".

Returns a ReconcileResult dataclass with valid, new_state, and errors fields.

# VERIFICATION_STAMP
# Story: 7.04
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 9/9
# Coverage: 100%
    )annotationsN)	dataclassfield)Optional)sqlite3API_KEYzsk-	list[str]FORBIDDEN_PATTERNSoppathzfrozenset[str]_REQUIRED_OP_KEYS>   addcopymovetestremovereplace
_VALID_OPSc                  F    e Zd ZU dZded<   dZded<    ee      Zded	<   y)
ReconcileResultz/Result of PatchReconciler.validate_and_apply().boolvalidNzOptional[dict]	new_state)default_factoryr	   errors)	__name__
__module____qualname____doc____annotations__r   r   listr        4/mnt/e/genesis-system/core/merge/patch_reconciler.pyr   r   3   s#    9K $I~$d3FI3r#   r   c                  N    e Zd ZdZddZd	dZ	 	 	 	 	 	 d
dZddZedd       Z	y)PatchReconcilera  
    Validates and applies an RFC 6902 JSON patch to a state dict.

    All three validation steps are always executed so the caller receives a
    complete list of errors rather than a fail-fast single error.

    Usage::

        reconciler = PatchReconciler()
        result = reconciler.validate_and_apply(current_state, patch_ops)
        if result.valid:
            commit(result.new_state)
        else:
            handle_errors(result.errors)
    c                *   g }| j                  |      }|j                  |       d}|s'| j                  ||      \  }}|j                  |       ndg}|"| j                  |      }|j                  |       |rt	        dd|      S t	        d|g       S )a  
        Run all three validation steps and return a ReconcileResult.

        Args:
            state: Current master state dict.  Not mutated regardless of
                   validation outcome.
            patch: List of RFC 6902 operation dicts to validate and apply.

        Returns:
            ReconcileResult with valid=True and new_state set on full success,
            or valid=False with errors list populated on any failure.
        Nu"   schema invalid — dry-run skippedF)r   r   r   T)_check_schemaextend_dry_run_apply_check_axiomsr   )selfstatepatchr   schema_errorsr   apply_errorsaxiom_errorss           r$   validate_and_applyz"PatchReconciler.validate_and_applyR   s     
 **51m$
 %)	&*&9&9%&G#I|MM,' AAL
  --i8LMM,'"$vNNTYrJJr#   c           	        g }t        |t              s)|j                  dt        |      j                          |S t        |      D ]  \  }}t        |t              s+|j                  d| dt        |      j                          At        t        |j                               z
  }|sd|j                  d| dt        |               |S )z
        Verify that *patch* is a list of dicts, each containing "op" and
        "path" keys.

        Returns a (possibly empty) list of error strings.
        zpatch must be a list, got patch[z] must be a dict, got z] missing required keys: )
isinstancer!   appendtyper   	enumeratedictr   setkeyssorted)r,   r.   r   ir   missings         r$   r(   zPatchReconciler._check_schema   s     %&MM,T%[-A-A,BC Mu% 	EArb$'QC5d2h6G6G5HI '#bggi.8GQC88IJ	 r#   c                V   t        j                  |      }g }t        |      D ]  \  }}	 | j                  ||      } |rd|fS |g fS # t        t
        t        t        f$ rG}|j                  d| d|j                  dd       d|j                  dd       d|        Y d}~d}~ww xY w)	u   
        Apply *patch* to a deep copy of *state* using RFC 6902 logic.

        The original *state* is never modified.

        Returns:
            (new_state, errors) — new_state is None if any operation failed.
        r4   z] (r   ? r   z): N)
r   deepcopyr8   _apply_single_opKeyError
IndexError	TypeError
ValueErrorr6   get)r,   r-   r.   working_stater   r=   op_dictexcs           r$   r*   zPatchReconciler._dry_run_apply   s     e,#E* 	JAw $ 5 5mW M	 <b   j)Z@ QCs7;;tS#9":!GKKPS<T;UUXe s   AB(!=B##B(c                    g }t        j                  |d      }t        D ]  }||v s|j                  d| d        |S )z
        Scan the serialised *new_state* for forbidden patterns.

        Returns a (possibly empty) list of error strings, one per pattern
        found.
        F)ensure_asciiz$axiom violation: forbidden pattern 'z' found in new_state)jsondumpsr
   r6   )r,   r   r   	state_strpatterns        r$   r+   zPatchReconciler._check_axioms   sP     JJyu=	) 	G)#:7)CWX	 r#   c                0   |j                  dd      j                         }|j                  dd      }|j                  d      }|j                  d      st        d|      |dd j	                  d      }|r|dgk(  rt        d	|      | }|dd
 D ]=  }t        |t              st        d|d|      ||vrt        d|d|      ||   }? |d
   }|dk(  r%t        |t              st        d|      |||<   | S |dk(  r:t        |t              st        d|      ||vrt        d|d|      |||<   | S |dk(  r8t        |t              st        d|      ||vrt        d|d|      ||= | S t        j                  d||       | S )u  
        Apply a single RFC 6902 operation to *state* (in-place on the supplied
        object) and return the modified state.

        Supported ops: add, replace, remove.
        Unsupported ops are silently skipped (no-op) to avoid breaking callers
        that pass move/copy/test operations.

        Path format: "/key" or "/parent/child/grandchild" (RFC 6902 pointer).
        Root path "/" maps to the root dict (not currently supported — will
        raise ValueError for safety).

        Raises:
            KeyError:   Path navigation fails (intermediate key missing).
            ValueError: Invalid path format or unsupported root-level op.
        r    r   value/zpath must start with '/', got:    Nz4root-level patch operations are not supported, path=zintermediate segment z is not a dict at path zintermediate key z not found at path r   z#'add' target is not a dict at path r   z''replace' target is not a dict at path z'replace' failed: key r   z&'remove' target is not a dict at path z'remove' failed: key zBPatchReconciler._apply_single_op: skipping unsupported op %r at %r)rH   lower
startswithrG   splitr5   r9   rD   rF   loggerdebug)	r-   r   op_typer   rT   segmentsparentsegleaf_keys	            r$   rC   z PatchReconciler._apply_single_op   s;   $ vvdB'--/FF62&w s#>thGHH 8>>#&8t+FthO 
 CR= 		!Cfd++C72I$R  & 'w.A$J  C[F		! B<efd+9$B   %F8@ = 	!fd+=dXF  v%,XL8KD8T   %F8* '  fd+<THE  v%+H<7J4(S  x   LLT r#   N)r-   r9   r.   r!   returnr   )r.   r!   rb   r	   )r-   r9   r.   r!   rb   z tuple[Optional[dict], list[str]])r   r9   rb   r	   )r-   r9   r   r9   rb   r9   )
r   r   r   r   r2   r(   r*   r+   staticmethodrC   r"   r#   r$   r&   r&   A   sM     *K`D!!"&!	)!@, V Vr#   r&   )r   
__future__r   r   rN   loggingdataclassesr   r   typingr   	getLoggerr   r[   r
   r    	frozensetr   r   r   r&   r"   r#   r$   <module>rj      s   2 #    ( 			8	$ !> I =$-tVn$= > =&'[\
N \ 4 4 4p pr#   