Skip to content

Commit 657daa1

Browse files
Pigbibicodex
andauthored
feat: add parked P5 shadow cycle controller (#5)
* feat: add parked P5 shadow cycle controller Co-Authored-By: Codex <noreply@openai.com> * test: cover later P5 shadow receipt chain Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 18e6077 commit 657daa1

4 files changed

Lines changed: 430 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ python -m alpaca_platform.shadow_cycle_input \
3737
输出同样采用 create-only 写入。后续 `shadow_ledger` 仍会重新验证整个 v2 input,而不是信任
3838
该适配器的成功输出。
3939

40+
`shadow_scheduler` 现提供了一个**未部署、无副作用**的 P5 控制步骤:如果前向观察、独立
41+
policy-gate receipt、风险摘要、deployment bundle 或前一账本回执缺失/无效,它只会返回
42+
`qsl.tqqq_shadow_scheduler_result.v1``PARKED` 原因码;只有它们全部有效时,才返回尚未
43+
持久化的虚拟账本回执(`RECEIPT_READY`)。它不排程、不抓取任何上游文件、不写存储、不连接
44+
Alpaca,也不代表 P5 已启用或 P4/P6 已获许可。后续部署会为该纯控制步骤单独接入受限的
45+
工件读取、create-only 回执写入与状态发布。
46+
4047
```bash
4148
python -m alpaca_platform.shadow_ledger --input cycle.json --output receipt.json
4249
```

src/alpaca_platform/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,27 @@
1111
validate_shadow_cycle_input,
1212
validate_shadow_ledger_receipt,
1313
)
14+
from .shadow_scheduler import (
15+
SCHEDULER_RESULT_SCHEMA,
16+
ShadowCycleOutcome,
17+
ShadowSchedulerError,
18+
run_tqqq_shadow_cycle,
19+
validate_shadow_scheduler_result,
20+
)
1421

1522
__all__ = [
1623
"FORWARD_OBSERVATION_SCHEMA",
1724
"POLICY_GATE_RECEIPT_SCHEMA",
25+
"SCHEDULER_RESULT_SCHEMA",
26+
"ShadowCycleOutcome",
1827
"ShadowLedgerError",
28+
"ShadowSchedulerError",
1929
"build_shadow_ledger_receipt",
2030
"build_tqqq_shadow_cycle_input",
2131
"calculate_forward_observation_sha256",
2232
"calculate_policy_gate_receipt_sha256",
33+
"run_tqqq_shadow_cycle",
2334
"validate_shadow_cycle_input",
2435
"validate_shadow_ledger_receipt",
36+
"validate_shadow_scheduler_result",
2537
]
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""Bounded, no-broker control step for one TQQQ P5 shadow cycle.
2+
3+
This module deliberately does not schedule itself, fetch upstream artifacts, or
4+
write a ledger. A future scheduler supplies already-local artifacts and can
5+
persist the returned receipt through a separate create-only adapter. Missing
6+
or invalid prerequisites produce a small, stable PARKED result instead of a
7+
retry/error loop or an implied authorization.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import re
13+
from dataclasses import dataclass
14+
from datetime import UTC, datetime
15+
from typing import Any
16+
17+
from .shadow_ledger import (
18+
ShadowLedgerError,
19+
build_shadow_ledger_receipt,
20+
build_tqqq_shadow_cycle_input,
21+
validate_shadow_ledger_receipt,
22+
)
23+
24+
SCHEDULER_RESULT_SCHEMA = "qsl.tqqq_shadow_scheduler_result.v1"
25+
26+
_IDENTITY = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
27+
_TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
28+
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
29+
_RESULT_FIELDS = {
30+
"schema",
31+
"cycle_id",
32+
"computed_at",
33+
"status",
34+
"reason_code",
35+
"shadow_receipt_sha256",
36+
}
37+
_PARKED_REASONS = {
38+
"forward_observation_missing",
39+
"policy_gate_receipt_missing",
40+
"risk_control_missing",
41+
"deployment_bundle_missing",
42+
"prior_receipt_invalid",
43+
"cycle_input_invalid",
44+
"ledger_receipt_invalid",
45+
}
46+
47+
48+
class ShadowSchedulerError(ValueError):
49+
"""Raised when the scheduler request/result shape is invalid."""
50+
51+
52+
@dataclass(frozen=True)
53+
class ShadowCycleOutcome:
54+
"""A safe control-plane result plus an optional unpersisted ledger receipt."""
55+
56+
result: dict[str, Any]
57+
receipt: dict[str, Any] | None
58+
59+
60+
def _identity(value: Any, label: str) -> str:
61+
if not isinstance(value, str) or not _IDENTITY.fullmatch(value):
62+
raise ShadowSchedulerError(f"{label} must be a lowercase immutable identity")
63+
return value
64+
65+
66+
def _timestamp(value: Any, label: str) -> str:
67+
if not isinstance(value, str) or not _TIMESTAMP.fullmatch(value):
68+
raise ShadowSchedulerError(f"{label} must be an RFC3339 UTC timestamp with whole seconds")
69+
try:
70+
datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
71+
except ValueError as exc:
72+
raise ShadowSchedulerError(f"{label} must be a valid calendar timestamp") from exc
73+
return value
74+
75+
76+
def validate_shadow_scheduler_result(value: Any) -> dict[str, Any]:
77+
"""Validate the small status artifact a scheduler may publish externally."""
78+
if not isinstance(value, dict):
79+
raise ShadowSchedulerError("shadow scheduler result must be an object")
80+
missing = sorted(_RESULT_FIELDS - set(value))
81+
unknown = sorted(set(value) - _RESULT_FIELDS)
82+
if missing:
83+
raise ShadowSchedulerError(f"shadow scheduler result missing required field(s): {', '.join(missing)}")
84+
if unknown:
85+
raise ShadowSchedulerError(f"shadow scheduler result has unknown field(s): {', '.join(unknown)}")
86+
if value["schema"] != SCHEDULER_RESULT_SCHEMA:
87+
raise ShadowSchedulerError(f"shadow scheduler result.schema must be {SCHEDULER_RESULT_SCHEMA}")
88+
89+
status = value["status"]
90+
reason_code = value["reason_code"]
91+
receipt_sha256 = value["shadow_receipt_sha256"]
92+
if status == "PARKED":
93+
if reason_code not in _PARKED_REASONS or receipt_sha256 is not None:
94+
raise ShadowSchedulerError("PARKED result must have a known reason and no receipt digest")
95+
elif status == "RECEIPT_READY":
96+
if (
97+
reason_code != "receipt_ready"
98+
or not isinstance(receipt_sha256, str)
99+
or not _DIGEST.fullmatch(receipt_sha256)
100+
):
101+
raise ShadowSchedulerError("RECEIPT_READY result must contain its receipt digest")
102+
else:
103+
raise ShadowSchedulerError("shadow scheduler result.status must be PARKED or RECEIPT_READY")
104+
return {
105+
"schema": SCHEDULER_RESULT_SCHEMA,
106+
"cycle_id": _identity(value["cycle_id"], "shadow scheduler result.cycle_id"),
107+
"computed_at": _timestamp(value["computed_at"], "shadow scheduler result.computed_at"),
108+
"status": status,
109+
"reason_code": reason_code,
110+
"shadow_receipt_sha256": receipt_sha256,
111+
}
112+
113+
114+
def _park(*, cycle_id: str, computed_at: str, reason_code: str) -> ShadowCycleOutcome:
115+
result = validate_shadow_scheduler_result(
116+
{
117+
"schema": SCHEDULER_RESULT_SCHEMA,
118+
"cycle_id": cycle_id,
119+
"computed_at": computed_at,
120+
"status": "PARKED",
121+
"reason_code": reason_code,
122+
"shadow_receipt_sha256": None,
123+
}
124+
)
125+
return ShadowCycleOutcome(result=result, receipt=None)
126+
127+
128+
def run_tqqq_shadow_cycle(
129+
*,
130+
cycle_id: str,
131+
computed_at: str,
132+
forward_observation: Any | None,
133+
policy_gate_receipt: Any | None,
134+
risk_control: Any | None,
135+
deployment_bundle_sha256: str | None,
136+
prior_receipt: Any | None = None,
137+
) -> ShadowCycleOutcome:
138+
"""Prepare one P5 receipt only when every bounded prerequisite is present.
139+
140+
Expected data absence and invalid external artifacts are intentionally
141+
represented as a closed, sanitized ``PARKED`` result. This is a pure
142+
control step: it neither persists the returned receipt nor accesses a
143+
broker, credential, network endpoint, or market-data source.
144+
"""
145+
normalized_cycle_id = _identity(cycle_id, "shadow scheduler cycle_id")
146+
normalized_computed_at = _timestamp(computed_at, "shadow scheduler computed_at")
147+
148+
if forward_observation is None:
149+
return _park(
150+
cycle_id=normalized_cycle_id,
151+
computed_at=normalized_computed_at,
152+
reason_code="forward_observation_missing",
153+
)
154+
if policy_gate_receipt is None:
155+
return _park(
156+
cycle_id=normalized_cycle_id,
157+
computed_at=normalized_computed_at,
158+
reason_code="policy_gate_receipt_missing",
159+
)
160+
if risk_control is None:
161+
return _park(
162+
cycle_id=normalized_cycle_id,
163+
computed_at=normalized_computed_at,
164+
reason_code="risk_control_missing",
165+
)
166+
if deployment_bundle_sha256 is None:
167+
return _park(
168+
cycle_id=normalized_cycle_id,
169+
computed_at=normalized_computed_at,
170+
reason_code="deployment_bundle_missing",
171+
)
172+
173+
if prior_receipt is not None:
174+
try:
175+
validate_shadow_ledger_receipt(prior_receipt)
176+
except ShadowLedgerError:
177+
return _park(
178+
cycle_id=normalized_cycle_id,
179+
computed_at=normalized_computed_at,
180+
reason_code="prior_receipt_invalid",
181+
)
182+
183+
try:
184+
cycle_input = build_tqqq_shadow_cycle_input(
185+
forward_observation=forward_observation,
186+
policy_gate_receipt=policy_gate_receipt,
187+
risk_control=risk_control,
188+
deployment_bundle_sha256=deployment_bundle_sha256,
189+
cycle_id=normalized_cycle_id,
190+
produced_at=normalized_computed_at,
191+
)
192+
except ShadowLedgerError:
193+
return _park(
194+
cycle_id=normalized_cycle_id,
195+
computed_at=normalized_computed_at,
196+
reason_code="cycle_input_invalid",
197+
)
198+
199+
try:
200+
receipt = build_shadow_ledger_receipt(cycle_input, prior_receipt=prior_receipt)
201+
except ShadowLedgerError:
202+
return _park(
203+
cycle_id=normalized_cycle_id,
204+
computed_at=normalized_computed_at,
205+
reason_code="ledger_receipt_invalid",
206+
)
207+
208+
result = validate_shadow_scheduler_result(
209+
{
210+
"schema": SCHEDULER_RESULT_SCHEMA,
211+
"cycle_id": normalized_cycle_id,
212+
"computed_at": normalized_computed_at,
213+
"status": "RECEIPT_READY",
214+
"reason_code": "receipt_ready",
215+
"shadow_receipt_sha256": receipt["receipt_sha256"],
216+
}
217+
)
218+
return ShadowCycleOutcome(result=result, receipt=receipt)

0 commit comments

Comments
 (0)