fix(rectification): dedupe same-domain probe years and drop unanchored style cards (BUG-559)

Pass asked probe keys into the engine without changing result fingerprints, block nearby years already asked, and require a dated same-domain ledger event before rendering varga_style cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 22:08:06 +08:00
parent 150d7ef189
commit 3a9ae736e1
8 changed files with 372 additions and 9 deletions
+5 -1
View File
@@ -183,7 +183,11 @@ def score_candidates(request: RectificationRequest) -> dict[str, Any]:
spec = calculation_spec(request)
spec_hash = sha256(spec)
diagnostic_values = run_diagnostics(scoring_request, rows, built)
fingerprint = sha256(request)
fingerprint = sha256({
key: value
for key, value in request.items()
if key != "asked_probe_keys"
})
result_id = str(uuid5(NAMESPACE_URL, f"{ALGORITHM_VERSION}:{fingerprint}"))
candidate_decisions = build_candidate_decisions(
rows,
+19 -1
View File
@@ -52,7 +52,7 @@ _EVENT_PROVENANCE_FIELDS = frozenset({
})
_REQUEST_FIELDS = frozenset({
"birth_date", "start_time", "end_time", "lat", "lon", "tz", "events",
"ayanamsa", "node_mode",
"ayanamsa", "node_mode", "asked_probe_keys",
}) | _REQUEST_PROVENANCE_FIELDS
_EVENT_FIELDS = frozenset({"id", "domain", "event_kind", "date_start", "date_end", "precision", "summary"}) | _EVENT_PROVENANCE_FIELDS
_CLOCK = re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d\Z")
@@ -88,6 +88,7 @@ class RectificationRequest(TypedDict):
timezone_id: NotRequired[str | None]
timezone_source: NotRequired[str | None]
local_time_status: NotRequired[str | None]
asked_probe_keys: NotRequired[list[str]]
JsonObject = dict[str, Any]
@@ -248,4 +249,21 @@ def normalize_rectification_request(body: Any, *, today: date | None = None) ->
_copy_nullable_text(body, cleaned_request, "timezone_id", "timezone_id", 120)
_copy_nullable_text(body, cleaned_request, "timezone_source", "timezone_source", 80)
_copy_nullable_text(body, cleaned_request, "local_time_status", "local_time_status", 120, _LOCAL_TIME_STATUSES)
if "asked_probe_keys" in body:
asked = body.get("asked_probe_keys")
if not isinstance(asked, list) or len(asked) > 200:
raise ValueError("asked_probe_keys must contain between 0 and 200 strings")
cleaned_keys: list[str] = []
seen: set[str] = set()
for index, item in enumerate(asked):
if not isinstance(item, str) or not item.strip() or len(item.strip()) > 120:
raise ValueError(
f"asked_probe_keys[{index}] must be a non-empty string up to 120 characters"
)
key = item.strip()
if key in seen:
continue
seen.add(key)
cleaned_keys.append(key)
cleaned_request["asked_probe_keys"] = cleaned_keys
return cast(RectificationRequest, cleaned_request)
+113 -4
View File
@@ -8,6 +8,7 @@ the case cap is respected. Unanchored quality stays clarification-only.
from __future__ import annotations
import re
from datetime import date, datetime, timedelta
from math import log2
from typing import Any, Sequence
@@ -220,6 +221,20 @@ EXISTENCE_NEARBY_YEARS = {
"career": 1,
"relocation": 1,
}
_SEMANTIC_YEAR = re.compile(r"^(?P<domain>[a-z_]+)\.(?P<year>(?:19|20)\d{2})(?:\.|$)")
def asked_years_for_domain(asked_probe_keys: Sequence[str] | None, domain: str) -> set[int]:
years: set[int] = set()
prefix = f"{domain}."
for raw in asked_probe_keys or []:
key = str(raw or "").strip()
if not key.startswith(prefix):
continue
match = _SEMANTIC_YEAR.match(key)
if match and match.group("domain") == domain:
years.add(int(match.group("year")))
return years
def _clock(value: str) -> int:
@@ -770,23 +785,38 @@ def _agent_brief(
family: str,
quality: bool = False,
exam: bool = False,
nearby_note: str = "",
) -> str:
nearby = nearby_note.strip()
if exam:
return (
f"时间范围锁定 {year_label};领域锁定 {domain}"
"语义目标是那次考试的实际体验。结合最近对话,只选一个容易回答的口语入口,"
"问是否明显失常或压力很大;不要堆叠例子,不得改时间范围。"
+ (f"{nearby}" if nearby else "")
)
if quality:
return (
f"时间范围锁定 {year_label};领域锁定 {domain};语义目标是 {family}"
"结合最近对话,只选一个容易回答的口语入口来核对体验;"
"不要逐字复述语义目标,不要堆叠例子,不得改时间范围。"
+ (f"{nearby}" if nearby else "")
)
lead = (
f"{nearby}时间范围锁定 {year_label};请问用户那段时间身上发生了什么变化;"
if nearby
else f"时间范围锁定 {year_label};领域锁定 {domain};语义目标是 {family}"
"结合最近对话,只选一个容易回答的口语入口,写一句自然的是/否题;"
)
if nearby:
return (
lead
+ f"领域锁定 {domain};语义目标是 {family}"
"选项由服务端给出;不要发明年份,不得改时间范围。"
)
return (
f"时间范围锁定 {year_label};领域锁定 {domain};语义目标是 {family}"
"结合最近对话,只选一个容易回答的口语入口,写一句自然的是/否题;"
"不要逐字复述语义目标,不要把所有例子堆进一句,不得改时间范围。"
lead
+ "不要逐字复述语义目标,不要把所有例子堆进一句,不得改时间范围。"
)
@@ -889,6 +919,79 @@ def _display_date_label(event: dict[str, Any]) -> str:
return f"{year}"
def _event_month_index(year: int, month: int | None) -> int | None:
if month is None or not 1 <= month <= 12:
return None
return year * 12 + month
def nearby_ledger_note(
events: Sequence[dict[str, Any]],
*,
domain: str,
year: int,
month: int | None,
) -> str:
if year <= 0:
return ""
probe_index = _event_month_index(year, month)
best: dict[str, Any] | None = None
best_delta = 99
for event in events:
if not isinstance(event, dict):
continue
other_domain = str(event.get("domain") or "")
if other_domain == domain or other_domain not in DOMAIN_CATALOG:
continue
other_year = _event_year(event)
if other_year is None:
continue
other_month = _event_month(event)
other_index = _event_month_index(other_year, other_month)
if probe_index is not None and other_index is not None:
delta = abs(probe_index - other_index)
if delta > 2:
continue
elif other_year != year:
continue
else:
delta = 2 if probe_index is not None or other_index is not None else 0
if delta < best_delta:
best_delta = delta
best = event
if best is None:
return ""
family = str(DOMAIN_CATALOG[str(best.get("domain") or "")]["event_family"])
return f"账本里 { _display_date_label(best) }{family};题干先提那件事再问。"
def _annotate_nearby_ledger(
probes: Sequence[dict[str, Any]],
events: Sequence[dict[str, Any]],
) -> None:
for probe in probes:
if not isinstance(probe, dict):
continue
if str(probe.get("choice_kind") or "existence") != "existence":
continue
if str(probe.get("source") or "") not in {"dasha_boundary", "dasha_activation"}:
continue
year = probe.get("year")
if not isinstance(year, int) or year <= 0:
continue
note = nearby_ledger_note(
events,
domain=str(probe.get("domain") or ""),
year=year,
month=int(probe["month"]) if isinstance(probe.get("month"), int) else None,
)
if not note:
continue
meaning = str(probe.get("user_meaning") or "")
if note not in meaning:
probe["user_meaning"] = f"{note}{meaning}"
def _event_kind_name(event: dict[str, Any]) -> str:
return str(event.get("event_kind") or event.get("kind") or "")
@@ -1402,6 +1505,11 @@ def _discriminating_event_probe_lists(
except ValueError:
return empty
events = [item for item in (request.get("events") or []) if isinstance(item, dict)]
asked_probe_keys = [
str(item).strip()
for item in (request.get("asked_probe_keys") or [])
if isinstance(item, str) and str(item).strip()
]
if not discriminator_gate_open(events):
return empty
holdout_keys = holdout_domain_years(events)
@@ -1438,7 +1546,7 @@ def _discriminating_event_probe_lists(
for domain in domains:
if domain not in DOMAIN_CATALOG:
continue
known_years = _event_years(events, domain)
known_years = _event_years(events, domain) | asked_years_for_domain(asked_probe_keys, domain)
blocked_years = _existence_blocked_years(domain, known_years)
domain_lo = _domain_year_floor(birth_year, domain, lo)
eligible = [
@@ -1506,6 +1614,7 @@ def _discriminating_event_probe_lists(
holdout_ids=set(holdout_event_ids(events)),
holdout_keys=set(holdout_keys),
))
_annotate_nearby_ledger(probes, events)
probes.sort(key=_probe_sort_key)
public, dropped = _partition_ranked_probes(probes)
assert_distinguish_contract(public)