@@ -35,6 +35,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
3535 run
3636 evidence
3737 route_evidence
38+ context_ai_summary
3839 events
3940 reviews
4041 review_decision
@@ -70,6 +71,20 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
7071 trace_ref
7172 )
7273
74+ @ required_context_ai_summary_paths [
75+ [ "context_packet" , "context_packet_ref" ] ,
76+ [ "context_packet" , "packet_hash" ] ,
77+ [ "context_packet" , "receipt_ref" ] ,
78+ [ "route_decision" , "route_decision_ref" ] ,
79+ [ "model_invocation" , "model_invocation_ref" ] ,
80+ [ "model_invocation" , "model_receipt_ref" ] ,
81+ [ "model_invocation" , "prompt_artifact_ref" ] ,
82+ [ "model_invocation" , "provider_payload_ref" ] ,
83+ [ "model_invocation" , "payload_hash" ] ,
84+ [ "eval_verdict" , "eval_verdict_ref" ] ,
85+ [ "operator_review" , "review_ref" ]
86+ ]
87+
7388 @ spec command_args ( ) :: [ String . t ( ) ]
7489 def command_args , do: @ product_args
7590
@@ -130,7 +145,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
130145 end
131146
132147 defp decode_product_receipt ( output ) do
133- case Jason . decode ( String . trim ( output ) ) do
148+ case decode_json_receipt ( output ) do
134149 { :ok , % { } = receipt } ->
135150 { :ok , receipt }
136151
@@ -146,6 +161,35 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
146161 end
147162 end
148163
164+ defp decode_json_receipt ( output ) do
165+ trimmed = String . trim ( output )
166+
167+ case Jason . decode ( trimmed ) do
168+ { :ok , decoded } -> { :ok , decoded }
169+ { :error , _reason } -> decode_trailing_json_receipt ( trimmed )
170+ end
171+ end
172+
173+ defp decode_trailing_json_receipt ( output ) do
174+ output
175+ |> :binary . matches ( "{" )
176+ |> Enum . map ( fn { position , 1 } ->
177+ binary_part ( output , position , byte_size ( output ) - position )
178+ end )
179+ |> Enum . reverse ( )
180+ |> Enum . find_value ( fn candidate ->
181+ case Jason . decode ( String . trim ( candidate ) ) do
182+ { :ok , % { "schema" => @ product_schema } = receipt } -> { :ok , receipt }
183+ { :ok , % { } = receipt } -> { :ok , receipt }
184+ _other -> nil
185+ end
186+ end )
187+ |> case do
188+ nil -> Jason . decode ( output )
189+ decoded -> decoded
190+ end
191+ end
192+
149193 defp validate_product_receipt ( % { } = receipt ) do
150194 with :ok <-
151195 require_equal ( receipt [ "schema" ] , @ product_schema , "extravaganza_receipt_bad_schema" ) ,
@@ -155,7 +199,8 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
155199 :ok <- require_required_refs ( receipt ) ,
156200 :ok <- require_lower_terminal_ref ( receipt ) ,
157201 :ok <- require_readbacks ( receipt ) ,
158- :ok <- require_route_evidence ( receipt ) do
202+ :ok <- require_route_evidence ( receipt ) ,
203+ :ok <- require_context_ai_summary ( receipt ) do
159204 require_projection_proof ( receipt )
160205 end
161206 end
@@ -245,6 +290,74 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
245290 end
246291 end
247292
293+ defp require_context_ai_summary ( receipt ) do
294+ summary = context_ai_summary ( receipt )
295+
296+ with :ok <- require_context_summary_present ( summary ) ,
297+ :ok <- require_context_summary_surface ( summary ) ,
298+ :ok <- require_context_summary_product_safe ( summary ) ,
299+ :ok <- require_context_summary_hashes ( summary ) do
300+ require_context_summary_refs ( summary )
301+ end
302+ end
303+
304+ defp require_context_summary_present ( % { } = summary ) when map_size ( summary ) > 0 , do: :ok
305+
306+ defp require_context_summary_present ( _summary ) do
307+ { :error , error ( "extravaganza_receipt_missing_context_ai_summary" ) }
308+ end
309+
310+ defp require_context_summary_surface ( % { "surface" => "AppKit.ContextSurface" } ) , do: :ok
311+
312+ defp require_context_summary_surface ( summary ) do
313+ { :error , error ( "extravaganza_receipt_bad_context_surface" , surface: summary [ "surface" ] ) }
314+ end
315+
316+ defp require_context_summary_product_safe ( summary ) do
317+ if summary [ "live_provider_required?" ] == false and summary [ "lower_stack_imports?" ] == false and
318+ summary [ "forbidden_raw_fields_present?" ] == false do
319+ :ok
320+ else
321+ context_summary_safety_error ( summary )
322+ end
323+ end
324+
325+ defp context_summary_safety_error ( % { "forbidden_raw_fields_present?" => value } )
326+ when value != false do
327+ { :error , error ( "extravaganza_receipt_context_summary_has_raw_fields" ) }
328+ end
329+
330+ defp context_summary_safety_error ( _summary ) do
331+ { :error , error ( "extravaganza_receipt_context_summary_not_product_safe" ) }
332+ end
333+
334+ defp require_context_summary_hashes ( summary ) do
335+ cond do
336+ not safe_sha? ( get_in ( summary , [ "context_packet" , "packet_hash" ] ) ) ->
337+ { :error , error ( "extravaganza_receipt_context_packet_hash_invalid" ) }
338+
339+ not safe_sha? ( get_in ( summary , [ "model_invocation" , "payload_hash" ] ) ) ->
340+ { :error , error ( "extravaganza_receipt_model_payload_hash_invalid" ) }
341+
342+ true ->
343+ :ok
344+ end
345+ end
346+
347+ defp require_context_summary_refs ( summary ) do
348+ missing =
349+ @ required_context_ai_summary_paths
350+ |> Enum . reject ( fn path -> safe_ref? ( get_in ( summary , path ) ) end )
351+ |> Enum . map ( & Enum . join ( & 1 , "." ) )
352+
353+ if missing == [ ] do
354+ :ok
355+ else
356+ { :error ,
357+ error ( "extravaganza_receipt_context_summary_missing_refs" , missing_fields: missing ) }
358+ end
359+ end
360+
248361 defp external_receipt ( product_receipt , opts ) do
249362 root = Keyword . get ( opts , :extravaganza_root , default_extravaganza_root ( ) )
250363 refs = refs ( product_receipt )
@@ -270,6 +383,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
270383 } ,
271384 "validated_refs" => validated_refs ( refs ) ,
272385 "validated_route_evidence" => route_evidence ( product_receipt ) ,
386+ "validated_context_ai_summary" => compact_context_summary ( product_receipt ) ,
273387 "product_receipt" => product_summary ( product_receipt ) ,
274388 "provider_smoke" => % {
275389 "classification" => "separate_provider_only_not_product_acceptance" ,
@@ -304,6 +418,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
304418 "readbacks" => Enum . map ( readbacks ( receipt ) , & & 1 [ "name" ] ) ,
305419 "steps" => proof_steps ( receipt ) ,
306420 "route_evidence" => route_evidence ( receipt ) ,
421+ "context_ai_summary" => compact_context_summary ( receipt ) ,
307422 "refs" =>
308423 Map . take (
309424 refs ( receipt ) ,
@@ -348,6 +463,49 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
348463
349464 defp route_evidence_readback ( _readback ) , do: nil
350465
466+ defp context_ai_summary ( receipt ) do
467+ direct = map_or_empty ( get_in ( receipt , [ "data" , "context_ai_summary" ] ) )
468+ proof_summary = map_or_empty ( get_in ( receipt , [ "data" , "proof" , "context_ai_summary" ] ) )
469+
470+ cond do
471+ direct != % { } ->
472+ direct
473+
474+ proof_summary != % { } ->
475+ proof_summary
476+
477+ true ->
478+ receipt
479+ |> readbacks ( )
480+ |> Enum . find_value ( % { } , & context_ai_summary_readback / 1 )
481+ end
482+ end
483+
484+ defp context_ai_summary_readback ( % { "name" => "context_ai_summary" } = readback ) do
485+ map_or_empty ( readback [ "data" ] )
486+ end
487+
488+ defp context_ai_summary_readback ( _readback ) , do: nil
489+
490+ defp compact_context_summary ( receipt ) do
491+ summary = context_ai_summary ( receipt )
492+
493+ % {
494+ "surface" => summary [ "surface" ] ,
495+ "proof_class" => summary [ "proof_class" ] ,
496+ "context_packet_ref" => get_in ( summary , [ "context_packet" , "context_packet_ref" ] ) ,
497+ "packet_hash" => get_in ( summary , [ "context_packet" , "packet_hash" ] ) ,
498+ "route_decision_ref" => get_in ( summary , [ "route_decision" , "route_decision_ref" ] ) ,
499+ "model_invocation_ref" => get_in ( summary , [ "model_invocation" , "model_invocation_ref" ] ) ,
500+ "model_receipt_ref" => get_in ( summary , [ "model_invocation" , "model_receipt_ref" ] ) ,
501+ "eval_verdict_ref" => get_in ( summary , [ "eval_verdict" , "eval_verdict_ref" ] ) ,
502+ "review_ref" => get_in ( summary , [ "operator_review" , "review_ref" ] ) ,
503+ "prompt_artifact_ref" => get_in ( summary , [ "model_invocation" , "prompt_artifact_ref" ] ) ,
504+ "provider_payload_ref" => get_in ( summary , [ "model_invocation" , "provider_payload_ref" ] ) ,
505+ "payload_hash" => get_in ( summary , [ "model_invocation" , "payload_hash" ] )
506+ }
507+ end
508+
351509 defp readbacks ( receipt ) do
352510 case proof ( receipt ) [ "readbacks" ] do
353511 readbacks when is_list ( readbacks ) -> readbacks
@@ -359,6 +517,8 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
359517 defp map_or_empty ( _value ) , do: % { }
360518
361519 defp safe_ref? ( value ) , do: is_binary ( value ) and String . trim ( value ) != ""
520+ defp safe_sha? ( "sha256:" <> hash ) , do: byte_size ( hash ) == 64
521+ defp safe_sha? ( _value ) , do: false
362522
363523 defp mix_executable , do: System . find_executable ( "mix" ) || "mix"
364524
0 commit comments