Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 123 additions & 10 deletions src/quant_platform_kit/strategy_lifecycle/research_promotion_cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import calendar
import copy
import hashlib
import json
import math
Expand Down Expand Up @@ -1321,6 +1322,14 @@ def _saved_proposal(raw: Mapping[str, Any]) -> OptimizationProposal:
_MAX_NO_IMPROVEMENT_ROUNDS = 3
_MAX_IDLE_DAYS = 30
_SUMMARY_MAX_TEXT = 2000
_SUMMARY_SEGMENT_MAX = 240
_SUMMARY_OLD_KEYS = frozenset({"status", "provider", "model", "text"})
_SUMMARY_NEW_KEYS = frozenset({"status", "provider", "model", "locales"})
_SUMMARY_LOCALE_FIELDS = ("question", "basis", "limits", "suggestion")
_SUMMARY_PROVIDERS = frozenset({"codex", "cursor"})
_HAN_RE = re.compile(r"[\u4e00-\u9fff]")
_LATIN_RE = re.compile(r"[A-Za-z]")
_DIGIT_RE = re.compile(r"[0-9\uff10-\uff19]")
_RESEARCH_OWNER_FIELDS = frozenset({"repository", "issue_number", "watcher_issue_key"})
_REPOSITORY_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
_RESEARCH_OWNER_KEY_RE = re.compile(r"^[A-Za-z0-9_-]{8,64}$")
Expand Down Expand Up @@ -1538,6 +1547,114 @@ def _proposal_comparable(proposal: OptimizationProposal) -> bool:
for value in current.cost_inputs.values())


def _same_ticket_material(left: Any, right: Any) -> bool:
try:
return _canonical_ticket_value(left) == _canonical_ticket_value(right)
except (TypeError, ValueError, OverflowError):
return False


def _candidate_summary_binding(
ticket: ResearchPromotionTicket,
proposal: OptimizationProposal | None,
context: Mapping[str, Any],
) -> dict[str, Any] | None:
"""Freeze candidate facts before a summary callback. None means do not call."""
identity = context.get("identity")
if not isinstance(identity, Mapping):
return None
if (identity.get("strategy_profile") != ticket.strategy_profile
or identity.get("domain") != ticket.domain
or not _same_ticket_material(identity.get("proposed_params"), ticket.proposed_params)):
return None
if proposal is not None and (
proposal.strategy_profile != ticket.strategy_profile
or proposal.domain != ticket.domain
or not _same_ticket_material(proposal.proposed_params, ticket.proposed_params)):
return None
raw = {
"ticket_id": ticket.ticket_id,
"strategy_profile": ticket.strategy_profile,
"domain": ticket.domain,
"proposed_params": dict(ticket.proposed_params),
"comparison": context.get("comparison"),
"shadow_evidence_kind": ticket.shadow_evidence_kind,
"shadow_passed": ticket.shadow_passed,
"notes": list(ticket.notes),
}
try:
frozen = json.loads(_canonical_ticket_value(raw))
except (TypeError, ValueError, OverflowError):
return None
return frozen if isinstance(frozen, dict) else None


def _summary_segment(text: Any, *, chinese: bool) -> str | None:
if not isinstance(text, str) or not text.strip() or len(text) > _SUMMARY_SEGMENT_MAX:
return None
if _DIGIT_RE.search(text):
return None
has_han = _HAN_RE.search(text) is not None
if chinese:
return text if has_han else None
if has_han or _LATIN_RE.search(text) is None:
return None
return text


def _bilingual_summary_explanation(
value: Mapping[str, Any], binding: Mapping[str, Any],
) -> dict[str, Any] | None:
provider = value.get("provider")
model = value.get("model")
locales = value.get("locales")
if (not isinstance(provider, str) or provider not in _SUMMARY_PROVIDERS
or not isinstance(model, str) or not model.strip()
or not isinstance(locales, Mapping) or frozenset(locales) != {"zh-CN", "en"}):
return None
saved_locales: dict[str, dict[str, str]] = {}
for name, chinese in (("zh-CN", True), ("en", False)):
section = locales[name]
if not isinstance(section, Mapping) or frozenset(section) != frozenset(_SUMMARY_LOCALE_FIELDS):
return None
saved: dict[str, str] = {}
for field_name in _SUMMARY_LOCALE_FIELDS:
segment = _summary_segment(section.get(field_name), chinese=chinese)
if segment is None:
return None
saved[field_name] = segment
saved_locales[name] = saved
return {
"status": "available",
"provider": provider,
"model": model,
"scope": "candidate",
"locales": saved_locales,
"binding": json.loads(_canonical_ticket_value(binding)),
}


def _accepted_summary_explanation(
value: Any, binding: Mapping[str, Any] | None,
) -> dict[str, Any] | None:
if not isinstance(value, Mapping) or value.get("status") != "available":
return None
keys = frozenset(value)
if keys == _SUMMARY_NEW_KEYS:
if binding is None:
return None
return _bilingual_summary_explanation(value, binding)
if not keys <= _SUMMARY_OLD_KEYS or "text" not in value:
return None
text = value.get("text")
provider = value.get("provider") or ""
model = value.get("model") or ""
if (isinstance(text, str) and text.strip() and len(text) <= _SUMMARY_MAX_TEXT
and provider == "codex" and isinstance(model, str)):
return {"status": "available", "text": text.strip(), "provider": "codex", "model": model}
return None


def _attach_research_summary(
ticket: ResearchPromotionTicket,
proposal: OptimizationProposal | None,
Expand All @@ -1560,17 +1677,13 @@ def _attach_research_summary(
ticket.research_summary = {**context, "ai_explanation": explanation}
if persist is not None:
persist()
if callable(summarize):
binding = _candidate_summary_binding(ticket, proposal, context) if callable(summarize) else None
if callable(summarize) and binding is not None:
try:
value = summarize(context)
if isinstance(value, Mapping) and value.get("status") == "available":
text = value.get("text")
provider = value.get("provider") or ""
model = value.get("model") or ""
if (isinstance(text, str) and text.strip() and len(text) <= _SUMMARY_MAX_TEXT
and provider == "codex" and isinstance(model, str)):
explanation = {"status": "available", "text": text.strip(),
"provider": "codex", "model": model}
value = summarize(copy.deepcopy(context))
accepted = _accepted_summary_explanation(value, binding)
if accepted is not None:
explanation = accepted
except Exception:
pass
ticket.research_summary = {**context, "ai_explanation": explanation}
Expand Down
203 changes: 203 additions & 0 deletions tests/test_research_promotion_resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,206 @@ def now(cls, tz=None):
assert admission.call_count == 1
ai.assert_not_called()
assert not list((tmp_path / "research_promotion_tickets").glob("*.json"))


def _summary_locale(*, chinese, **overrides):
fields = {
"question": "该候选与基线在固定窗口下是否接近?",
"basis": "比较来自同一窗口与同一成本条件。",
"limits": "这只是候选建议,尚未选定账户。",
"suggestion": "请人工审阅,不构成执行授权。",
} if chinese else {
"question": "Does this candidate stay close to the baseline?",
"basis": "The comparison uses one fixed window and one cost model.",
"limits": "This is candidate advice before any account is selected.",
"suggestion": "Ask a human to review it without execution authority.",
}
fields.update(overrides)
return fields


def _bilingual_summary(provider="codex", model="synthetic-summary-model", locales=None, **extra):
payload = {
"status": "available",
"provider": provider,
"model": model,
"locales": locales if locales is not None else {
"zh-CN": _summary_locale(chinese=True),
"en": _summary_locale(chinese=False),
},
}
payload.update(extra)
return payload


def _candidate_cycle(tmp_path, summarize):
return invoke(
tmp_path,
optimize=Mock(return_value=_comparable_proposal(recommendation="research_candidate")),
summarize=summarize,
)


def test_one_callback_saves_both_locales_and_reuses_without_recall(tmp_path):
summarize = Mock(return_value=_bilingual_summary())
result = _candidate_cycle(tmp_path, summarize)
saved = cycle.load_research_promotion_ticket(result["ticket_path"])
explanation = saved.research_summary["ai_explanation"]
context = summarize.call_args.args[0]
assert summarize.call_count == 1
assert set(context) == {"identity", "strategy_description", "plugins", "comparison", "limitations"}
assert context["identity"]["proposed_params"] == {"a": 2}
assert explanation["status"] == "available"
assert explanation["provider"] == "codex"
assert explanation["model"] == "synthetic-summary-model"
assert explanation["scope"] == "candidate"
assert set(explanation["locales"]) == {"zh-CN", "en"}
assert set(explanation["locales"]["zh-CN"]) == {"question", "basis", "limits", "suggestion"}
assert explanation["binding"]["ticket_id"] == saved.ticket_id
assert explanation["binding"]["proposed_params"] == {"a": 2}
assert explanation["binding"]["comparison"] == saved.research_summary["comparison"]
assert explanation["binding"]["shadow_evidence_kind"] == saved.shadow_evidence_kind
assert explanation["binding"]["shadow_passed"] is True
assert explanation["binding"]["notes"] == list(saved.notes)
assert "selected_account" not in explanation
assert saved.state == cycle.ResearchPromotionState.AWAITING_HUMAN
assert saved.live_authority_granted is False
assert saved.human_decision == ""
_candidate_cycle(tmp_path, summarize)
assert summarize.call_count == 1
reread = cycle.load_research_promotion_ticket(result["ticket_path"])
assert reread.research_summary["ai_explanation"] == explanation
assert reread.live_authority_granted is False
assert reread.state == cycle.ResearchPromotionState.AWAITING_HUMAN


@pytest.mark.parametrize("provider", ["codex", "cursor"])
def test_legal_single_provider_bilingual_summary_is_saved(tmp_path, provider):
summarize = Mock(return_value=_bilingual_summary(provider=provider))
result = _candidate_cycle(tmp_path, summarize)
saved = cycle.load_research_promotion_ticket(result["ticket_path"])
explanation = saved.research_summary["ai_explanation"]
assert summarize.call_count == 1
assert explanation["provider"] == provider
assert explanation["scope"] == "candidate"
assert explanation["locales"]["en"]["question"].startswith("Does this candidate")
assert saved.live_authority_granted is False
assert saved.human_decision == ""


def test_saved_plain_text_summary_is_not_rewritten_as_bilingual(tmp_path):
old = Mock(return_value={"status": "available", "text": "候选与基线在固定窗口下接近。",
"provider": "codex", "model": "gpt-test"})
result = _candidate_cycle(tmp_path, old)
fresh = Mock(return_value=_bilingual_summary(provider="cursor"))
_candidate_cycle(tmp_path, fresh)
saved = cycle.load_research_promotion_ticket(result["ticket_path"])
explanation = saved.research_summary["ai_explanation"]
assert old.call_count == 1
assert fresh.call_count == 0
assert explanation == {"status": "available", "text": "候选与基线在固定窗口下接近。",
"provider": "codex", "model": "gpt-test"}


def test_summary_callback_cannot_mutate_persisted_binding_or_facts(tmp_path):
def mutate(context):
context["identity"]["strategy_profile"] = "hijacked"
context["identity"]["proposed_params"]["a"] = 9
context["comparison"]["status"] = "hijacked"
context["limitations"].append("hijacked")
return _bilingual_summary()

summarize = Mock(side_effect=mutate)
result = _candidate_cycle(tmp_path, summarize)
saved = cycle.load_research_promotion_ticket(result["ticket_path"])
assert saved.strategy_profile == "demo_strategy"
assert saved.proposed_params == {"a": 2}
assert saved.research_summary["identity"]["proposed_params"] == {"a": 2}
assert saved.research_summary["comparison"]["status"] == "comparable"
assert saved.research_summary["limitations"] == []
binding = saved.research_summary["ai_explanation"]["binding"]
assert binding["strategy_profile"] == "demo_strategy"
assert binding["proposed_params"] == {"a": 2}
assert binding["comparison"]["status"] == "comparable"
assert saved.live_authority_granted is False


def test_mismatched_proposal_does_not_call_summary_or_change_eligibility(tmp_path):
result = _candidate_cycle(tmp_path, None)
path = cycle.Path(result["ticket_path"])
saved = cycle.load_research_promotion_ticket(path)
saved.research_summary = {
key: value for key, value in saved.research_summary.items() if key != "ai_explanation"
}
saved.proposed_params = {"a": 99}
cycle.save_research_promotion_ticket(saved, path)
summarize = Mock(return_value=_bilingual_summary())
second = _candidate_cycle(tmp_path, summarize)
reread = cycle.load_research_promotion_ticket(path)
assert summarize.call_count == 0
assert second["status"] == "awaiting_human"
assert reread.proposed_params == {"a": 99}
assert reread.research_summary["ai_explanation"]["status"] == "unavailable"
assert "locales" not in reread.research_summary["ai_explanation"]
assert reread.state == cycle.ResearchPromotionState.AWAITING_HUMAN
assert reread.live_authority_granted is False
assert reread.human_decision == ""


@pytest.mark.parametrize("payload", [
_bilingual_summary(provider="other-model"),
_bilingual_summary(model=""),
_bilingual_summary(model=" "),
_bilingual_summary(locales={"zh-CN": _summary_locale(chinese=True)}),
_bilingual_summary(locales={
"zh-CN": {key: value for key, value in _summary_locale(chinese=True).items() if key != "limits"},
"en": _summary_locale(chinese=False),
}),
_bilingual_summary(binding={"ticket_id": "forged"}),
_bilingual_summary(locales={
"zh-CN": {**_summary_locale(chinese=True), "account": "not-an-account"},
"en": _summary_locale(chinese=False),
}),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True),
"en": _summary_locale(chinese=False, suggestion="See item 3"),
}),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True, suggestion="请看第3点"),
"en": _summary_locale(chinese=False),
}),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True, question=1),
"en": _summary_locale(chinese=False),
}),
_bilingual_summary(locales=["zh-CN", "en"]),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True, question="问" * 241),
"en": _summary_locale(chinese=False),
}),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True),
"en": _summary_locale(chinese=False, question="该候选 is close"),
}),
_bilingual_summary(locales={
"zh-CN": _summary_locale(chinese=True, question="Is this close?"),
"en": _summary_locale(chinese=False),
}),
{**_bilingual_summary(), "text": "候选与基线在固定窗口下接近。"},
_bilingual_summary(scope="account"),
])
def test_invalid_bilingual_summary_stays_unavailable_and_is_not_retried(tmp_path, payload):
summarize = Mock(return_value=payload)
result = _candidate_cycle(tmp_path, summarize)
_candidate_cycle(tmp_path, summarize)
saved = cycle.load_research_promotion_ticket(result["ticket_path"])
explanation = saved.research_summary["ai_explanation"]
assert summarize.call_count == 1
assert explanation["status"] == "unavailable"
assert explanation["text"] == "暂无法生成 AI 简述。"
assert "locales" not in explanation
assert "binding" not in explanation
assert "scope" not in explanation
assert saved.state == cycle.ResearchPromotionState.AWAITING_HUMAN
assert saved.live_authority_granted is False
assert saved.human_decision == ""
Loading