11"""Create-only, no-broker persistence seam for bounded P5 shadow receipts.
22
33This module runs only after ``shadow_scheduler``. It wraps an already
4- validated ``RECEIPT_READY`` result in a closed admission artifact and exposes a
5- small create-only storage port. The included in-memory implementation is for
6- tests/local deterministic replay only: it has no filesystem, network,
7- credential, market-data, broker, or scheduler dependency.
4+ validated ``RECEIPT_READY`` result in a closed admission artifact only after a
5+ separately produced deterministic risk decision passes its bounded adapter.
6+ It exposes a small create-only storage port. The included in-memory
7+ implementation is for tests/local deterministic replay only: it has no
8+ filesystem, network, credential, market-data, broker, or scheduler dependency.
89"""
910
1011from __future__ import annotations
1617from typing import Any , Protocol
1718
1819from .shadow_ledger import ShadowLedgerError , validate_shadow_ledger_receipt
20+ from .shadow_risk_gate_decision import (
21+ ShadowRiskGateDecisionError ,
22+ ShadowRiskGateDecisionMismatchError ,
23+ ShadowRiskGateDecisionProhibitedError ,
24+ build_p5_risk_gate_decision_reference ,
25+ validate_p5_risk_gate_decision_reference ,
26+ )
1927from .shadow_scheduler import (
2028 ShadowCycleOutcome ,
2129 ShadowSchedulerError ,
2230 validate_shadow_scheduler_result ,
2331)
2432
25- ADMISSION_SCHEMA = "qsl.tqqq_shadow_receipt_admission.v1 "
33+ ADMISSION_SCHEMA = "qsl.tqqq_shadow_receipt_admission.v2 "
2634PERSISTENCE_RESULT_SCHEMA = "qsl.tqqq_shadow_receipt_persistence_result.v1"
2735
2836_DIGEST = re .compile (r"^[0-9a-f]{64}$" )
3240 "computed_at" ,
3341 "scheduler_result" ,
3442 "shadow_receipt" ,
43+ "risk_gate_decision" ,
3544 "admission_sha256" ,
3645}
3746_PERSISTENCE_RESULT_FIELDS = {
5160 "prior_receipt_invalid" ,
5261 "cycle_input_invalid" ,
5362 "ledger_receipt_invalid" ,
63+ "risk_gate_decision_missing" ,
64+ "risk_gate_decision_invalid" ,
65+ "risk_gate_decision_mismatch" ,
66+ "risk_gate_decision_prohibited" ,
5467 "receipt_conflict" ,
5568}
5669
@@ -139,24 +152,40 @@ def validate_shadow_receipt_admission(value: Any) -> dict[str, Any]:
139152 _fail ("shadow receipt admission computed_at must match controller result" )
140153 if result ["shadow_receipt_sha256" ] != receipt ["receipt_sha256" ]:
141154 _fail ("shadow receipt admission scheduler result must bind ledger receipt digest" )
155+ try :
156+ risk_gate_decision = validate_p5_risk_gate_decision_reference (
157+ value ["risk_gate_decision" ],
158+ expected_source_risk_control = receipt ["risk_control" ],
159+ expected_computed_at = result ["computed_at" ],
160+ )
161+ except ShadowRiskGateDecisionMismatchError as exc :
162+ raise ShadowReceiptStoreError ("shadow receipt admission risk-gate decision does not match this P5 receipt" ) from exc
163+ except ShadowRiskGateDecisionError as exc :
164+ raise ShadowReceiptStoreError ("shadow receipt admission risk-gate decision is invalid" ) from exc
142165 normalized = {
143166 "schema" : ADMISSION_SCHEMA ,
144167 "cycle_id" : result ["cycle_id" ],
145168 "computed_at" : result ["computed_at" ],
146169 "scheduler_result" : result ,
147170 "shadow_receipt" : receipt ,
171+ "risk_gate_decision" : risk_gate_decision ,
148172 "admission_sha256" : _digest (value ["admission_sha256" ], "shadow receipt admission.admission_sha256" ),
149173 }
150174 if normalized ["admission_sha256" ] != calculate_shadow_receipt_admission_sha256 (normalized ):
151175 _fail ("shadow receipt admission.admission_sha256 mismatch" )
152176 return normalized
153177
154178
155- def build_shadow_receipt_admission (outcome : ShadowCycleOutcome ) -> dict [str , Any ]:
179+ def build_shadow_receipt_admission (
180+ outcome : ShadowCycleOutcome ,
181+ * ,
182+ risk_gate_decision : Any ,
183+ ) -> dict [str , Any ]:
156184 """Convert one controller ``RECEIPT_READY`` outcome into an admission.
157185
158- This function cannot create a ledger receipt. A parked result, a missing
159- receipt, or an inconsistent outcome is rejected before a store is called.
186+ This function cannot create a ledger receipt or a risk decision. A parked
187+ result, a missing receipt, a prohibited decision, or an inconsistent
188+ binding is rejected before a store is called.
160189 """
161190 try :
162191 result = validate_shadow_scheduler_result (outcome .result )
@@ -166,12 +195,23 @@ def build_shadow_receipt_admission(outcome: ShadowCycleOutcome) -> dict[str, Any
166195 _fail ("only a RECEIPT_READY controller outcome can be admitted" )
167196 if outcome .receipt is None :
168197 _fail ("RECEIPT_READY controller outcome must include a ledger receipt" )
198+ try :
199+ receipt = validate_shadow_ledger_receipt (outcome .receipt )
200+ except ShadowLedgerError as exc :
201+ raise ShadowReceiptStoreError ("RECEIPT_READY controller outcome ledger receipt is invalid" ) from exc
202+ risk_gate_reference = build_p5_risk_gate_decision_reference (
203+ risk_gate_decision ,
204+ expected_cycle_id = result ["cycle_id" ],
205+ expected_computed_at = result ["computed_at" ],
206+ expected_source_risk_control = receipt ["risk_control" ],
207+ )
169208 admission : dict [str , Any ] = {
170209 "schema" : ADMISSION_SCHEMA ,
171210 "cycle_id" : result ["cycle_id" ],
172211 "computed_at" : result ["computed_at" ],
173212 "scheduler_result" : result ,
174- "shadow_receipt" : outcome .receipt ,
213+ "shadow_receipt" : receipt ,
214+ "risk_gate_decision" : risk_gate_reference ,
175215 "admission_sha256" : "" ,
176216 }
177217 admission ["admission_sha256" ] = calculate_shadow_receipt_admission_sha256 (admission )
@@ -269,11 +309,15 @@ def create_if_absent(self, admission: Mapping[str, Any]) -> bool:
269309def persist_shadow_cycle_outcome (
270310 outcome : ShadowCycleOutcome ,
271311 store : CreateOnlyShadowReceiptStore ,
312+ * ,
313+ risk_gate_decision : Any | None = None ,
272314) -> dict [str , Any ]:
273315 """Persist a ready P5 receipt once, or return a closed parked/reconciled result.
274316
275- It never promotes P5, retries missing inputs, or writes a parked result. A
276- conflicting immutable cycle returns ``PARKED`` and remains untouched.
317+ It never promotes P5, resets a breaker, retries missing inputs, or writes a
318+ parked result. A missing, prohibited, or mismatched deterministic risk
319+ decision returns ``PARKED`` before the store is read or called. A
320+ conflicting immutable cycle also returns ``PARKED`` and remains untouched.
277321 """
278322 try :
279323 controller_result = validate_shadow_scheduler_result (outcome .result )
@@ -291,7 +335,44 @@ def persist_shadow_cycle_outcome(
291335 admission_sha256 = None ,
292336 )
293337
294- admission = build_shadow_receipt_admission (outcome )
338+ if risk_gate_decision is None :
339+ return _persistence_result (
340+ cycle_id = controller_result ["cycle_id" ],
341+ computed_at = controller_result ["computed_at" ],
342+ status = "PARKED" ,
343+ reason_code = "risk_gate_decision_missing" ,
344+ shadow_receipt_sha256 = None ,
345+ admission_sha256 = None ,
346+ )
347+ try :
348+ admission = build_shadow_receipt_admission (outcome , risk_gate_decision = risk_gate_decision )
349+ except ShadowRiskGateDecisionProhibitedError :
350+ return _persistence_result (
351+ cycle_id = controller_result ["cycle_id" ],
352+ computed_at = controller_result ["computed_at" ],
353+ status = "PARKED" ,
354+ reason_code = "risk_gate_decision_prohibited" ,
355+ shadow_receipt_sha256 = None ,
356+ admission_sha256 = None ,
357+ )
358+ except ShadowRiskGateDecisionMismatchError :
359+ return _persistence_result (
360+ cycle_id = controller_result ["cycle_id" ],
361+ computed_at = controller_result ["computed_at" ],
362+ status = "PARKED" ,
363+ reason_code = "risk_gate_decision_mismatch" ,
364+ shadow_receipt_sha256 = None ,
365+ admission_sha256 = None ,
366+ )
367+ except ShadowRiskGateDecisionError :
368+ return _persistence_result (
369+ cycle_id = controller_result ["cycle_id" ],
370+ computed_at = controller_result ["computed_at" ],
371+ status = "PARKED" ,
372+ reason_code = "risk_gate_decision_invalid" ,
373+ shadow_receipt_sha256 = None ,
374+ admission_sha256 = None ,
375+ )
295376 cycle_id = admission ["cycle_id" ]
296377 existing = store .read (cycle_id )
297378 created = existing is None and store .create_if_absent (admission )
0 commit comments