Skip to content

Commit c5b73ba

Browse files
authored
Merge pull request #139 from QuantStrategyLab/codex/reconciliation-baseline-dual-review
feat: require dual review for reconciliation baselines
2 parents b20f583 + e7db4f9 commit c5b73ba

9 files changed

Lines changed: 145 additions & 24 deletions
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# 旧实盘基线:强制双 AI 审计
2+
3+
本流程处理已经获授权运行、但由于迁移、配置漂移或运行异常而进入
4+
`RECONCILE_ONLY` 的旧实盘实例。它不是策略研究晋级流程,也不会修改订单、仓位或
5+
资金。
6+
7+
## 输入边界
8+
9+
平台适配器只能在只读 broker 会话中生成 QPK 的
10+
`broker_reconciliation_evidence.v1`。原始账户、现金、持仓、订单和成交明细必须留在
11+
受控环境;传入审计的只可以是 QPK 生成的
12+
`broker_reconciliation_baseline_candidate.v1` 中的摘要和时间窗。
13+
14+
候选至少需要两次间隔采样且所有状态摘要一致。候选的内容地址
15+
`candidate_sha256` 是后续审计和控制面操作的唯一绑定值。
16+
17+
## 审计门槛
18+
19+
调用 `run_dual_review_pipeline.py --trigger reconciliation_baseline` 时,必须提供:
20+
21+
- `strategy_profile`
22+
- `reconciliation_candidate_sha256`
23+
- 不包含敏感资料的候选摘要,例如来源收据数量和观察窗口。
24+
25+
该触发器不会采用“主审置信度足够即可放行”的普通优化。它始终运行 Codex 主审与
26+
独立 GPT/Claude 复审;缺少候选绑定、任一审计不可用、结论分歧或拒绝都不能通过。
27+
输出中的 `evidence_binding_sha256` 必须与候选的 `candidate_sha256` 完全一致。
28+
29+
## 私有控制面职责
30+
31+
审计通过仍不等于恢复实盘。只有私有、受权限保护的控制面可以:
32+
33+
1. 验证候选和每份来源收据来自受信任的运行时,且内容地址未变;
34+
2. 验证双审结果为通过,并绑定同一个候选摘要;
35+
3. 在同一 runtime target 上原子写入五个预期状态摘要并转换到 `ACTIVE_LKG`
36+
4. 保存不可变审计记录、操作人/服务身份、时间和目标版本,以便回滚和复核。
37+
38+
任一条件不满足,控制面必须保持 `RECONCILE_ONLY`。AIAuditBridge 不持有券商密钥,
39+
也不拥有下单或直接修改 Cloud Run runtime target 的权限。

scripts/run_dual_review_pipeline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,10 @@ def _exit_code(result: dict[str, Any]) -> int:
163163

164164
def main(argv: list[str] | None = None) -> int:
165165
parser = argparse.ArgumentParser(description="Run Codex primary + dual API secondary review pipeline.")
166-
parser.add_argument("--trigger", choices=("promotion", "hit_rate", "drift"))
166+
parser.add_argument(
167+
"--trigger",
168+
choices=("promotion", "hit_rate", "drift", "reconciliation_baseline"),
169+
)
167170
parser.add_argument("--strategy-profile")
168171
parser.add_argument("--context-json", default="{}", help="Inline JSON or file path for trigger context")
169172
parser.add_argument("--evidence-file", help="Evidence package path (promotion)")

service/dual_review.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
VERDICT_INVALID = "invalid_review"
1919

2020
DEFAULT_ESCALATION_THRESHOLD = 0.8
21+
MANDATORY_MULTI_REVIEW_TRIGGERS = frozenset({"reconciliation_baseline"})
2122

2223
_PASS_VALUES = frozenset({"pass", "approve", "approved", "accept", "accepted"})
2324
_FAIL_VALUES = frozenset({"fail", "reject", "rejected", "deny", "denied", "block", "blocked"})
@@ -31,6 +32,14 @@ class DualReviewTrigger(str, Enum):
3132
PROMOTION = "promotion"
3233
HIT_RATE = "hit_rate"
3334
DRIFT = "drift"
35+
RECONCILIATION_BASELINE = "reconciliation_baseline"
36+
37+
38+
def requires_mandatory_multi_review(trigger: DualReviewTrigger | str) -> bool:
39+
"""Return whether a decision may never be approved by one reviewer alone."""
40+
41+
value = trigger.value if isinstance(trigger, DualReviewTrigger) else str(trigger).strip().lower()
42+
return value in MANDATORY_MULTI_REVIEW_TRIGGERS
3443

3544

3645
def _normalize_verdict(value: Any) -> str | None:

service/dual_review_orchestrator.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import os
6+
import re
67
from dataclasses import dataclass, field
78
from typing import Any, Callable
89

@@ -16,6 +17,7 @@
1617
compare_three_reviews,
1718
extract_confidence,
1819
extract_verdict,
20+
requires_mandatory_multi_review,
1921
should_escalate,
2022
)
2123
from service.dual_review_secondary import dual_api_secondary_reviewer, is_dual_api_secondary
@@ -24,6 +26,7 @@
2426
from service.model_router import route_model
2527

2628
SecondaryReviewer = Callable[["DualReviewRequest"], dict[str, Any]]
29+
_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")
2730

2831

2932
@dataclass(frozen=True)
@@ -33,6 +36,7 @@ class DualReviewRequest:
3336
primary_review: dict[str, Any]
3437
context: dict[str, Any] = field(default_factory=dict)
3538
escalation_threshold: float = DEFAULT_ESCALATION_THRESHOLD
39+
evidence_binding_sha256: str | None = None
3640

3741

3842
@dataclass
@@ -46,6 +50,7 @@ class DualReviewResult:
4650
outcome: str = ""
4751
reason: str = ""
4852
model_route: dict[str, str] = field(default_factory=dict)
53+
evidence_binding_sha256: str | None = None
4954

5055
def to_dict(self) -> dict[str, Any]:
5156
return {
@@ -58,6 +63,7 @@ def to_dict(self) -> dict[str, Any]:
5863
"outcome": self.outcome,
5964
"reason": self.reason,
6065
"model_route": self.model_route,
66+
"evidence_binding_sha256": self.evidence_binding_sha256,
6167
}
6268

6369

@@ -77,12 +83,19 @@ def build_request_from_payload(payload: dict[str, Any]) -> DualReviewRequest | N
7783
except (TypeError, ValueError):
7884
cutoff = DEFAULT_ESCALATION_THRESHOLD
7985
context = {k: v for k, v in payload.items() if k not in {"primary_review", "secondary_review"}}
86+
evidence_binding_sha256 = None
87+
if trigger is DualReviewTrigger.RECONCILIATION_BASELINE:
88+
candidate = str(payload.get("reconciliation_candidate_sha256") or "").strip().lower()
89+
if not _SHA256_PATTERN.fullmatch(candidate):
90+
return None
91+
evidence_binding_sha256 = candidate
8092
return DualReviewRequest(
8193
trigger=trigger,
8294
strategy_profile=strategy_profile,
8395
primary_review=primary,
8496
context=context,
8597
escalation_threshold=cutoff,
98+
evidence_binding_sha256=evidence_binding_sha256,
8699
)
87100

88101

@@ -127,27 +140,32 @@ def orchestrate_dual_review(
127140
strategy_profile=request.strategy_profile,
128141
primary_review=request.primary_review,
129142
model_route=dict(route),
143+
evidence_binding_sha256=request.evidence_binding_sha256,
130144
)
131145

132-
primary_confidence = extract_confidence(request.primary_review)
133-
if primary_confidence is None:
146+
if requires_mandatory_multi_review(request.trigger):
134147
result.escalated = True
135-
result.reason = "primary confidence missing; escalating"
136-
elif should_escalate(primary_confidence, threshold=request.escalation_threshold):
137-
result.escalated = True
138-
result.reason = f"primary confidence {primary_confidence:.2f} below {request.escalation_threshold:.2f}"
148+
result.reason = "mandatory multi-review for reconciliation baseline enrollment"
139149
else:
140-
primary_verdict = extract_verdict(request.primary_review)
141-
if primary_verdict == VERDICT_PASS:
142-
result.outcome = VERDICT_PASS
143-
result.reason = "primary confidence sufficient; approved without secondary review"
144-
return result
145-
if primary_verdict == VERDICT_FAIL:
146-
result.outcome = VERDICT_FAIL
147-
result.reason = "primary confidence sufficient; rejected without secondary review"
148-
return result
149-
result.escalated = True
150-
result.reason = "primary verdict unclear; escalating"
150+
primary_confidence = extract_confidence(request.primary_review)
151+
if primary_confidence is None:
152+
result.escalated = True
153+
result.reason = "primary confidence missing; escalating"
154+
elif should_escalate(primary_confidence, threshold=request.escalation_threshold):
155+
result.escalated = True
156+
result.reason = f"primary confidence {primary_confidence:.2f} below {request.escalation_threshold:.2f}"
157+
else:
158+
primary_verdict = extract_verdict(request.primary_review)
159+
if primary_verdict == VERDICT_PASS:
160+
result.outcome = VERDICT_PASS
161+
result.reason = "primary confidence sufficient; approved without secondary review"
162+
return result
163+
if primary_verdict == VERDICT_FAIL:
164+
result.outcome = VERDICT_FAIL
165+
result.reason = "primary confidence sufficient; rejected without secondary review"
166+
return result
167+
result.escalated = True
168+
result.reason = "primary verdict unclear; escalating"
151169

152170
secondary = reviewer(request)
153171
result.secondary_review = secondary

service/dual_review_primary.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from service.dual_review_secondary import parse_llm_review_output
1414

1515
_PRIMARY_SYSTEM = (
16-
"You are the primary Codex reviewer for quantitative strategy promotion and risk decisions. "
16+
"You are the primary Codex reviewer for quantitative strategy promotion, risk, and recovery decisions. "
1717
"Respond with JSON only: "
1818
'{"verdict":"approve"|"reject","confidence":0.0-1.0,"summary":"..."}'
1919
)
@@ -37,6 +37,9 @@ def build_primary_prompt(
3737
"drift_sigma",
3838
"drift_score",
3939
"repository",
40+
"reconciliation_candidate_sha256",
41+
"source_evidence_count",
42+
"observation_window_seconds",
4043
):
4144
value = context.get(key)
4245
if value not in (None, ""):
@@ -65,7 +68,9 @@ def build_primary_prompt(
6568
except (OSError, json.JSONDecodeError):
6669
lines.append(f"evidence_file: {evidence_path}")
6770
lines.append(
68-
"Provide an independent primary review for whether this strategy change should proceed."
71+
"Provide an independent primary review. For reconciliation_baseline, approve only when the "
72+
"candidate is a fresh, matching, read-only observation set; this decision still requires "
73+
"independent secondary approval and never authorises orders."
6974
)
7075
return "\n".join(lines)
7176

service/dual_review_secondary.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,16 @@ def build_secondary_prompt(request: DualReviewRequest) -> str:
8080
f"Strategy profile: {request.strategy_profile}",
8181
f"Trigger: {request.trigger.value}",
8282
]
83-
for key in ("domain", "old_status", "new_status", "drift_sigma", "drift_score"):
83+
for key in (
84+
"domain",
85+
"old_status",
86+
"new_status",
87+
"drift_sigma",
88+
"drift_score",
89+
"reconciliation_candidate_sha256",
90+
"source_evidence_count",
91+
"observation_window_seconds",
92+
):
8493
if key in context and context[key] not in (None, ""):
8594
lines.append(f"{key}: {context[key]}")
8695
monthly = context.get("monthly_hit_rates")
@@ -90,8 +99,9 @@ def build_secondary_prompt(request: DualReviewRequest) -> str:
9099
if evidence:
91100
lines.append(f"context: {evidence}")
92101
lines.append(
93-
"Decide whether this strategy change should proceed. "
94-
"Use approve only when evidence supports promotion or continued operation."
102+
"Decide whether this strategy change should proceed. For reconciliation_baseline, approve "
103+
"only when the candidate is fresh, read-only, internally consistent, and safely scoped; "
104+
"approval never authorises orders or a state change by itself."
95105
)
96106
return "\n".join(lines)
97107

tests/test_dual_review.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
DualReviewTrigger,
1515
compare_reviews,
1616
compare_three_reviews,
17+
requires_mandatory_multi_review,
1718
should_escalate,
1819
)
1920

@@ -140,11 +141,16 @@ def test_trigger_values(self) -> None:
140141
self.assertEqual(DualReviewTrigger.PROMOTION.value, "promotion")
141142
self.assertEqual(DualReviewTrigger.HIT_RATE.value, "hit_rate")
142143
self.assertEqual(DualReviewTrigger.DRIFT.value, "drift")
144+
self.assertEqual(DualReviewTrigger.RECONCILIATION_BASELINE.value, "reconciliation_baseline")
143145

144146
def test_trigger_is_string_enum(self) -> None:
145147
self.assertIsInstance(DualReviewTrigger.PROMOTION, str)
146148
self.assertEqual(DualReviewTrigger("drift"), DualReviewTrigger.DRIFT)
147149

150+
def test_reconciliation_baseline_requires_multiple_reviewers(self) -> None:
151+
self.assertTrue(requires_mandatory_multi_review(DualReviewTrigger.RECONCILIATION_BASELINE))
152+
self.assertFalse(requires_mandatory_multi_review(DualReviewTrigger.PROMOTION))
153+
148154

149155
if __name__ == "__main__":
150156
unittest.main()

tests/test_dual_review_orchestrator.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,37 @@ def test_primary_high_confidence_fail(self) -> None:
107107
self.assertEqual(result.outcome, VERDICT_FAIL)
108108
self.assertFalse(result.escalated)
109109

110+
def test_reconciliation_baseline_forces_independent_review_and_binds_candidate(self) -> None:
111+
candidate_sha256 = "a" * 64
112+
result = orchestrate_from_payload(
113+
{
114+
"trigger": "reconciliation_baseline",
115+
"strategy_profile": "soxl_soxx_trend_income",
116+
"reconciliation_candidate_sha256": candidate_sha256,
117+
"primary_review": {"verdict": "approve", "confidence": 0.99},
118+
},
119+
secondary_review={
120+
"gpt": {"verdict": "approve", "confidence": 0.95},
121+
"claude": {"verdict": "approve", "confidence": 0.94},
122+
},
123+
)
124+
125+
assert result is not None
126+
self.assertTrue(result.escalated)
127+
self.assertEqual(result.outcome, VERDICT_PASS)
128+
self.assertEqual(result.evidence_binding_sha256, candidate_sha256)
129+
130+
def test_reconciliation_baseline_requires_candidate_binding(self) -> None:
131+
request = build_request_from_payload(
132+
{
133+
"trigger": "reconciliation_baseline",
134+
"strategy_profile": "soxl_soxx_trend_income",
135+
"primary_review": {"verdict": "approve", "confidence": 0.99},
136+
}
137+
)
138+
139+
self.assertIsNone(request)
140+
110141

111142
if __name__ == "__main__":
112143
unittest.main()

tests/test_dual_review_primary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def test_budget_error_is_unavailable(self, review) -> None:
125125
result = run_codex_primary_review(prompt="review")
126126
self.assertEqual(result["verdict"], VERDICT_UNAVAILABLE)
127127
expected_prompt = (
128-
"You are the primary Codex reviewer for quantitative strategy promotion and risk decisions. "
128+
"You are the primary Codex reviewer for quantitative strategy promotion, risk, and recovery decisions. "
129129
"Respond with JSON only: "
130130
'{"verdict":"approve"|"reject","confidence":0.0-1.0,"summary":"..."}'
131131
"\n\nreview"

0 commit comments

Comments
 (0)