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:
@@ -457,6 +457,7 @@ function engineRequestBody(input: {
|
||||
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
|
||||
candidateRange: { start_time: string; end_time: string };
|
||||
events: readonly V9EngineEvent[];
|
||||
askedProbeKeys?: readonly string[];
|
||||
}): Record<string, unknown> {
|
||||
const snapshot = input.baselineBirthSnapshot;
|
||||
const birthDate = String(snapshot.birth_date ?? "");
|
||||
@@ -483,6 +484,7 @@ function engineRequestBody(input: {
|
||||
timezone_id: snapshot.timezone_id,
|
||||
timezone_source: snapshot.timezone_source,
|
||||
local_time_status: snapshot.local_time_status,
|
||||
...(input.askedProbeKeys?.length ? { asked_probe_keys: [...input.askedProbeKeys] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -595,6 +597,7 @@ export async function runV9CandidateScore(input: {
|
||||
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
|
||||
candidateRange: { start_time: string; end_time: string };
|
||||
events: readonly V9EngineEvent[];
|
||||
askedProbeKeys?: readonly string[];
|
||||
}): Promise<V9EngineScoreResult> {
|
||||
const data = await postEngine("/api/rectification/v5/score", engineRequestBody(input));
|
||||
const candidates = readCandidates(data.candidate_decisions, input.candidateRange);
|
||||
@@ -632,6 +635,7 @@ export async function runV9Diagnostics(input: {
|
||||
baselineBirthSnapshot: Readonly<Record<string, unknown>>;
|
||||
candidateRange: { start_time: string; end_time: string };
|
||||
events: readonly V9EngineEvent[];
|
||||
askedProbeKeys?: readonly string[];
|
||||
}): Promise<V9EngineDiagnostics> {
|
||||
const data = await postEngine("/api/rectification/v5/diagnostics", engineRequestBody(input));
|
||||
const candidates = readCandidates(data.candidate_decisions, input.candidateRange);
|
||||
|
||||
@@ -34,7 +34,8 @@ export type ProbeRejectReason =
|
||||
| "insufficient_candidates"
|
||||
| "insufficient_outcomes"
|
||||
| "yearless_ungrounded_contrast"
|
||||
| "no_split_among_active";
|
||||
| "no_split_among_active"
|
||||
| "unanchored_varga_style";
|
||||
|
||||
export type StyleOptionsResult =
|
||||
| { ok: true; options: ProbeStyleOption[] }
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
evidenceSubjectForDomain,
|
||||
applyOccupationCollectLedgerNorm,
|
||||
} from "@/lib/rectification-agentic/v9/evidence-model";
|
||||
import { USER_COLLECT_QUESTION } from "@/lib/rectification-agentic/user-copy";
|
||||
import { GENERIC_COLLECT_QUESTION, USER_COLLECT_QUESTION } from "@/lib/rectification-agentic/user-copy";
|
||||
import {
|
||||
isHoldoutVerificationQuote,
|
||||
} from "@/lib/rectification-agentic/v9/choice-card";
|
||||
@@ -839,6 +839,10 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
compute.baselineProfileFingerprint,
|
||||
);
|
||||
const events = toEngineEvents(scorableEvidence(dossier.evidence));
|
||||
const askedProbeKeys = askedDiscriminatorKeys(
|
||||
dossier.latestResult?.decisionReceipt,
|
||||
parsed.evidence,
|
||||
);
|
||||
const latest = dossier.latestResult;
|
||||
const liveIdentity = await readV9EngineScoringIdentity();
|
||||
if (
|
||||
@@ -940,6 +944,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
baselineBirthSnapshot: compute.baselineBirthSnapshot,
|
||||
candidateRange: parsed.case.candidateRange,
|
||||
events,
|
||||
askedProbeKeys,
|
||||
});
|
||||
const engineCompareMs = Date.now() - scoreStarted;
|
||||
const vedastroStarted = Date.now();
|
||||
@@ -1263,7 +1268,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
resultFingerprint: JSON.stringify({ reason: spoken.reason }),
|
||||
});
|
||||
const fallbackDomain = nextFollowup.domain ?? "";
|
||||
const fallbackPrompt = USER_COLLECT_QUESTION[fallbackDomain] ?? USER_COLLECT_QUESTION.other;
|
||||
const fallbackPrompt = USER_COLLECT_QUESTION[fallbackDomain] ?? GENERIC_COLLECT_QUESTION;
|
||||
if (
|
||||
spokenPromptFailures < 2
|
||||
|| nextFollowup.intent !== "collect_method_evidence"
|
||||
@@ -1910,6 +1915,10 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
baselineBirthSnapshot: compute.baselineBirthSnapshot,
|
||||
candidateRange: parsed.case.candidateRange,
|
||||
events: toEngineEvents(scorableEvidence(dossier.evidence)),
|
||||
askedProbeKeys: askedDiscriminatorKeys(
|
||||
dossier.latestResult?.decisionReceipt,
|
||||
parsed.evidence,
|
||||
),
|
||||
});
|
||||
const projection = {
|
||||
engine_result_id: diagnostics.engineResultId,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildMethodFollowupPlan,
|
||||
datedLedgerAnchor,
|
||||
existenceProbeAsked,
|
||||
remainingReverseVerifyProbes,
|
||||
type MethodFollowupEvidence,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
import type { CandidateContrastPacket } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
|
||||
function existenceProbe(
|
||||
domain: DiscriminatingEventProbe["domain"],
|
||||
year: number,
|
||||
extra: { month?: number; source?: DiscriminatingEventProbe["source"]; key?: string } = {},
|
||||
): DiscriminatingEventProbe {
|
||||
const month = extra.month;
|
||||
const source = extra.source ?? "dasha_boundary";
|
||||
const key = extra.key
|
||||
?? (month
|
||||
? `${domain}.${year}.${String(month).padStart(2, "0")}.${source}`
|
||||
: `${domain}.${year}.${source}`);
|
||||
return {
|
||||
year,
|
||||
year_label: month ? `${year} 年 ${month} 月前后` : `${year} 年前后`,
|
||||
month,
|
||||
domain,
|
||||
event_family: "入职、升职或职责明显加重",
|
||||
source,
|
||||
tracks: ["vimshottari", "narayana"],
|
||||
tracks_agree: true,
|
||||
unique_minute_claim: false,
|
||||
user_meaning: `时间范围锁定 ${year} 年。`,
|
||||
role: "distinguish",
|
||||
information_gain: 1.1,
|
||||
semantic_key: key,
|
||||
candidate_split_hash: key,
|
||||
candidate_ids: ["05:00", "05:20"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] },
|
||||
{ answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] },
|
||||
],
|
||||
choice_kind: "existence",
|
||||
};
|
||||
}
|
||||
|
||||
function dated(
|
||||
id: string,
|
||||
domain: string,
|
||||
eventKind: string,
|
||||
occurredFrom: string,
|
||||
): MethodFollowupEvidence {
|
||||
return {
|
||||
id,
|
||||
status: "confirmed",
|
||||
domain,
|
||||
datePrecision: "month",
|
||||
occurredFrom,
|
||||
occurredTo: null,
|
||||
eventKind,
|
||||
};
|
||||
}
|
||||
|
||||
const D10_STYLE: CandidateContrastPacket["probes"][number] = {
|
||||
probeId: "contrast:varga.d10.巨蟹座/狮子座",
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
question: "平时做事,你更接近下面哪一种?",
|
||||
expectedOutcomes: [
|
||||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] },
|
||||
],
|
||||
candidateSplitHash: "varga.d10.巨蟹座/狮子座",
|
||||
informationGain: 1.4,
|
||||
sourceFeatures: [{ technique: "D10", calculationResultId: null }],
|
||||
domain: "career",
|
||||
year: null,
|
||||
semanticKey: "varga.d10.巨蟹座/狮子座",
|
||||
choiceKind: "varga_style",
|
||||
styleOptions: [
|
||||
{ label: "做事以照顾人为主,在意团队里的感受", answerClass: "yes", sign: "巨蟹座" },
|
||||
{ label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" },
|
||||
],
|
||||
};
|
||||
|
||||
test("existenceProbeAsked treats the same career year and nearby years as already asked", () => {
|
||||
const asked = ["career.2018.05.dasha_boundary"];
|
||||
assert.equal(existenceProbeAsked(asked, "career", 2018), true);
|
||||
assert.equal(existenceProbeAsked(asked, "career", 2017), true);
|
||||
assert.equal(existenceProbeAsked(asked, "career", 2019), true);
|
||||
assert.equal(existenceProbeAsked(asked, "career", 2020), false);
|
||||
assert.equal(existenceProbeAsked(asked, "finance", 2018), false);
|
||||
});
|
||||
|
||||
test("remaining reverse-verify probes drop same-domain nearby years after a month probe", () => {
|
||||
const remaining = remainingReverseVerifyProbes(
|
||||
[
|
||||
existenceProbe("career", 2018, { month: 5 }),
|
||||
existenceProbe("career", 2018, { source: "dasha_activation" }),
|
||||
existenceProbe("career", 2017, { month: 5 }),
|
||||
existenceProbe("career", 2019, { month: 5 }),
|
||||
existenceProbe("career", 2020, { month: 5 }),
|
||||
],
|
||||
[],
|
||||
new Set(),
|
||||
new Set(["career.2018.05.dasha_boundary"]),
|
||||
);
|
||||
const years = remaining.filter((item) => item.domain === "career").map((item) => item.year);
|
||||
assert.equal(years.includes(2017), false);
|
||||
assert.equal(years.includes(2018), false);
|
||||
assert.equal(years.includes(2019), false);
|
||||
assert.equal(years.includes(2020), true);
|
||||
});
|
||||
|
||||
test("datedLedgerAnchor names the confirmed same-domain month", () => {
|
||||
const anchor = datedLedgerAnchor([
|
||||
dated("e-career-month", "career", "career_entry", "2018-07-01"),
|
||||
], "career");
|
||||
assert.ok(anchor);
|
||||
assert.equal(anchor?.label, "2018 年 7 月");
|
||||
assert.equal(datedLedgerAnchor([
|
||||
dated("e-edu", "education", "education_start", "2016-09-01"),
|
||||
], "career"), null);
|
||||
});
|
||||
|
||||
test("unanchored D10 varga_style cards are dropped; anchored cards mention the ledger month", () => {
|
||||
const baseEvidence = [
|
||||
dated("e-edu", "education", "education_start", "2016-09-01"),
|
||||
dated("e-edu-2", "education", "education_completion", "2020-06-01"),
|
||||
dated("e-rel", "relationship", "relationship_start", "2024-05-01"),
|
||||
dated("e-fin", "finance", "finance_loss", "2021-01-01"),
|
||||
];
|
||||
const packet = {
|
||||
candidateSetVersion: "05:00-05:20",
|
||||
vargaDifferences: [] as const,
|
||||
probes: [D10_STYLE],
|
||||
};
|
||||
const unanchored = buildMethodFollowupPlan({
|
||||
evidence: baseEvidence,
|
||||
contrastPacket: packet,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(
|
||||
unanchored.dropped_probes.some((item) => item.reason === "unanchored_varga_style"),
|
||||
true,
|
||||
);
|
||||
assert.notEqual(unanchored.next_followup?.semantic_key, D10_STYLE.semanticKey);
|
||||
|
||||
const anchored = buildMethodFollowupPlan({
|
||||
evidence: [...baseEvidence, dated("e-career", "career", "career_entry", "2018-07-01")],
|
||||
contrastPacket: packet,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey);
|
||||
assert.equal(anchored.next_followup?.choice_kind, "varga_style");
|
||||
assert.match(anchored.next_followup?.user_prompt_hint ?? "", /2018 年 7 月/);
|
||||
assert.doesNotMatch(anchored.next_followup?.choice_frame?.prompt ?? "", /2018/);
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1060,6 +1060,65 @@ class QualityDistinguishDedupeTests(unittest.TestCase):
|
||||
])
|
||||
self.assertEqual(len(over_cap), MAX_QUALITY_DISTINGUISH_PROBES)
|
||||
|
||||
def test_asked_career_month_probe_blocks_same_and_nearby_years(self) -> None:
|
||||
from scripts.rectification.event_probes import (
|
||||
asked_years_for_domain,
|
||||
_existence_blocked_years,
|
||||
nearby_ledger_note,
|
||||
)
|
||||
|
||||
years = asked_years_for_domain(["career.2022.05.dasha_boundary"], "career")
|
||||
self.assertEqual(years, {2022})
|
||||
self.assertEqual(_existence_blocked_years("career", years), {2021, 2022, 2023})
|
||||
self.assertEqual(asked_years_for_domain(["career.2022.05.dasha_boundary"], "finance"), set())
|
||||
note = nearby_ledger_note(
|
||||
[{
|
||||
"domain": "relocation",
|
||||
"date_start": "2022-07-01",
|
||||
"precision": "month",
|
||||
}],
|
||||
domain="career",
|
||||
year=2022,
|
||||
month=5,
|
||||
)
|
||||
self.assertIn("2022 年 7 月", note)
|
||||
self.assertNotIn("summary", note)
|
||||
|
||||
def test_asked_career_month_probe_is_not_reemitted_for_same_or_nearby_year(self) -> None:
|
||||
from unittest.mock import patch
|
||||
|
||||
from scripts.rectification import event_probes as probes_mod
|
||||
|
||||
built = {
|
||||
"static_contexts": [
|
||||
_context("05:13", d4_asc=0, sun_house=4, sun_varga_sign=3, moon=100.0),
|
||||
_context("05:40", d4_asc=1, sun_house=10, sun_varga_sign=9, moon=101.0),
|
||||
]
|
||||
}
|
||||
|
||||
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
|
||||
return [date(2018, 5, 15)] if moon <= 100.0 else [date(2019, 5, 20)]
|
||||
|
||||
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
|
||||
moon = float(planets.get("Moon") or 0)
|
||||
return [date(2018, 5, 15)] if moon <= 100.0 else [date(2019, 5, 20)]
|
||||
|
||||
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
|
||||
del birth_date, domain, context
|
||||
if year in {2018, 2019} and month == 5:
|
||||
return {"rule_ids": ["vim_ad_domain_lord"]}
|
||||
return {"rule_ids": ["no_domain_activation"]}
|
||||
|
||||
request = _request(asked_probe_keys=["career.2018.05.dasha_boundary"])
|
||||
with (
|
||||
patch.object(probes_mod, "_vim_start_dates", side_effect=fake_vim),
|
||||
patch.object(probes_mod, "_narayana_start_dates", side_effect=fake_narayana),
|
||||
patch.object(probes_mod, "_score_year", side_effect=fake_score),
|
||||
):
|
||||
probes = _probes(request, built, ["05:13", "05:40"], "05:13")
|
||||
career = [item for item in probes if item["domain"] == "career"]
|
||||
self.assertFalse(any(item["year"] in {2017, 2018, 2019} for item in career), career)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user