33from __future__ import annotations
44
55import json
6+ from collections .abc import Callable
67from typing import cast
78
89from scripts .compiler_differential .constants import MISSING_VALUE , VALUE_PREVIEW_CHARACTERS
910from scripts .compiler_differential .models import Divergence
11+ from scripts .compiler_differential .types import NodeResolvers
12+ from sqlbuild .compiler .frontier .constants import STAGE_CAPTURE_SHARED_MARKER
1013
1114
12- def first_json_difference (* , left : object , right : object , pointer : str = "" ) -> Divergence | None :
13- """Return the first JSON pointer whose value, type, or object key order differs."""
15+ def first_json_difference (
16+ * , left : object , right : object , pointer : str = "" , resolve : NodeResolvers | None = None
17+ ) -> Divergence | None :
18+ """Return the first JSON pointer whose value, type, or key order differs, via `resolve`."""
1419
20+ if resolve is not None :
21+ left_digest : str | None = shared_digest (left )
22+ if left_digest is not None and left_digest == shared_digest (right ):
23+ return None
24+ left , right = resolve [0 ](left ), resolve [1 ](right )
1525 if type (left ) is not type (right ):
1626 return _divergence (location = pointer or "/" , left = left , right = right )
1727 left_object : dict [str , object ] | None = as_json_object (left )
1828 right_object : dict [str , object ] | None = as_json_object (right )
1929 if left_object is not None and right_object is not None :
20- return _first_object_difference (left = left_object , right = right_object , pointer = pointer )
30+ return _first_object_difference (
31+ left = left_object , right = right_object , pointer = pointer , resolve = resolve
32+ )
2133 if isinstance (left , list ) and isinstance (right , list ):
2234 for index , (left_item , right_item ) in enumerate (zip (left , right , strict = False )):
2335 found : Divergence | None = first_json_difference (
24- left = left_item , right = right_item , pointer = f"{ pointer } /{ index } "
36+ left = left_item , right = right_item , pointer = f"{ pointer } /{ index } " , resolve = resolve
2537 )
2638 if found is not None :
2739 return found
2840 if len (left ) != len (right ):
2941 index : int = min (len (left ), len (right ))
3042 return _divergence (
3143 location = f"{ pointer } /{ index } " ,
32- left = left [index ] if index < len (left ) else MISSING_VALUE ,
33- right = right [index ] if index < len (right ) else MISSING_VALUE ,
44+ left = _shown (value = left [index ], resolve = resolve , side = 0 )
45+ if index < len (left )
46+ else MISSING_VALUE ,
47+ right = _shown (value = right [index ], resolve = resolve , side = 1 )
48+ if index < len (right )
49+ else MISSING_VALUE ,
3450 )
3551 return None
3652 if left != right :
@@ -50,20 +66,19 @@ def first_text_difference(*, left: str, right: str) -> Divergence | None:
5066 right_line : str = right_lines [index ] if index < len (right_lines ) else MISSING_VALUE
5167 if left_line != right_line :
5268 return Divergence (
53- location = f"line { index + 1 } " , left = _preview (left_line ), right = _preview (right_line )
69+ location = f"line { index + 1 } " , left = preview (left_line ), right = preview (right_line )
5470 )
5571 return Divergence (location = "end of file" , left = "<bytes differ>" , right = "<bytes differ>" )
5672
5773
58- def first_document_difference ( * , left : str , right : str ) -> Divergence | None :
59- """Compare two documents as JSON when both parse, otherwise line by line ."""
74+ def shared_digest ( value : object ) -> str | None :
75+ """Return the digest a stage-capture `{"__shared__": digest}` reference names ."""
6076
61- try :
62- left_payload : object = json .loads (left )
63- right_payload : object = json .loads (right )
64- except json .JSONDecodeError :
65- return first_text_difference (left = left , right = right )
66- return first_json_difference (left = left_payload , right = right_payload )
77+ mapping : dict [str , object ] | None = as_json_object (value )
78+ if mapping is None or len (mapping ) != 1 :
79+ return None
80+ digest : object = mapping .get (STAGE_CAPTURE_SHARED_MARKER )
81+ return digest if isinstance (digest , str ) else None
6782
6883
6984def as_json_object (value : object ) -> dict [str , object ] | None :
@@ -79,35 +94,70 @@ def json_pointer_token(key: str) -> str:
7994
8095
8196def _first_object_difference (
82- * , left : dict [str , object ], right : dict [str , object ], pointer : str
97+ * ,
98+ left : dict [str , object ],
99+ right : dict [str , object ],
100+ pointer : str ,
101+ resolve : NodeResolvers | None ,
83102) -> Divergence | None :
84103 for key in (* left , * (key for key in right if key not in left )):
85104 location : str = f"{ pointer } /{ json_pointer_token (key )} "
86105 if key not in left or key not in right :
87106 return _divergence (
88107 location = location ,
89- left = left .get (key , MISSING_VALUE ),
90- right = right .get (key , MISSING_VALUE ),
108+ left = _shown ( value = left .get (key , MISSING_VALUE ), resolve = resolve , side = 0 ),
109+ right = _shown ( value = right .get (key , MISSING_VALUE ), resolve = resolve , side = 1 ),
91110 )
92111 found : Divergence | None = first_json_difference (
93- left = left [key ], right = right [key ], pointer = location
112+ left = left [key ], right = right [key ], pointer = location , resolve = resolve
94113 )
95114 if found is not None :
96115 return found
97116 if list (left ) != list (right ):
98117 return Divergence (
99118 location = f"{ pointer or '/' } (key order)" ,
100- left = _preview (list (left )),
101- right = _preview (list (right )),
119+ left = preview (list (left )),
120+ right = preview (list (right )),
102121 )
103122 return None
104123
105124
125+ def _shown (* , value : object , resolve : NodeResolvers | None , side : int ) -> object :
126+ if resolve is None :
127+ return value
128+ return _previewed (value = value , resolve = resolve [side ], remaining = VALUE_PREVIEW_CHARACTERS )[0 ]
129+
130+
131+ def _previewed (
132+ * , value : object , resolve : Callable [[object ], object ], remaining : int
133+ ) -> tuple [object , int ]:
134+ if remaining <= 0 :
135+ return value , remaining
136+ resolved : object = resolve (value )
137+ mapping : dict [str , object ] | None = as_json_object (resolved )
138+ if mapping is not None :
139+ shown : dict [str , object ] = {}
140+ for key , item in mapping .items ():
141+ shown [key ], remaining = _previewed (
142+ value = item , resolve = resolve , remaining = remaining - len (key )
143+ )
144+ return shown , remaining
145+ if isinstance (resolved , list ):
146+ items : list [object ] = []
147+ for item in resolved :
148+ shown_item , remaining = _previewed (value = item , resolve = resolve , remaining = remaining )
149+ items .append (shown_item )
150+ return items , remaining
151+ return resolved , remaining - len (json .dumps (resolved , ensure_ascii = False ))
152+
153+
106154def _divergence (* , location : str , left : object , right : object ) -> Divergence :
107- return Divergence (location = location , left = _preview (left ), right = _preview (right ))
155+ return Divergence (location = location , left = preview (left ), right = preview (right ))
156+
108157
158+ def preview (value : object ) -> str :
159+ """Render a value for a report, shortened to the preview width."""
109160
110- def _preview (value : object ) -> str :
111161 rendered : str = (
112162 value
113163 if isinstance (value , str ) and value == MISSING_VALUE
0 commit comments