fix(rectification): ask reverse-inference probes at engine dasha months
Independent Staging Quality Gate / validate (push) Failing after 8m39s
Independent Staging Quality Gate / publish (push) Has been skipped

Keep Vimshottari/Narayana start dates instead of truncating to year, so
same-year month splits can appear on the choice card.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-28 10:20:05 +08:00
parent 4767b34ff0
commit 68a78af9e1
14 changed files with 359 additions and 84 deletions
+16
View File
@@ -6221,3 +6221,19 @@
- 复发自:BUG-405(排序公式对,目录被投影饿死,已打开低分卡锁题)
- 修复版本:待发布
## BUG-408 | 反推题丢掉大运起点的月份,同年 3 月对 9 月问不出来
- 状态:resolved
- 首次发现:2026-08-28
- 最近更新:2026-08-28
- 影响面:`scripts/rectification/event_probes.py`、点选卡 `choice_frame.period`、Skill `10.0.13` 区分阶段时间范围
- 用户现象:反推点选卡只写「YYYY 年前后」。候选分钟只差十几三十分钟时,大运/副运起点其实已经错开几个月,但题目仍按年问;同一年里 3 月对 9 月这种差被丢掉。
- 触发条件:训练事件已够、剩余候选的 Vimshottari/Narayana 大运或副运起点相差不足一年,但月份已分开。
- 根因:探针只取起点 `.year`,边界还要求两边年份至少差 1 年,评分固定落在当年 7 月 1 日。引擎算到的月日被扔掉。
- 修复:边界改用大运/副运实际起点。年份不同,或同年且相差至少 45 天,按该月出题并按该月计分。标签写成「YYYY 年 M 月前后」。年龄带兜底仍只锁年。点选卡直接用探针 `year_label`,不得再从年份拼回「年前后」。开场仍不诱导用户猜月份。Skill 版本保持 `10.0.13`
- 验证:Python 事件探针回归锁定同年 3 月/9 月边界、微小同年漂移仍不出边界题、年龄带仍是年精度;前端点选卡与 method-followup 锁定「2018 年 3 月前后」。
- 防复发:`dasha_boundary` 必须带引擎月份;不得把起点截成年后再比。年龄带/无起点时不得伪造月份。
- 相关记录:BUG-405、BUG-407
- 复发自:无
- 修复版本:待发布
@@ -729,7 +729,7 @@ function remainingQuestion(
layer: string,
layerLabel: string,
): string {
return `引擎给出的区分机会绑定 ${layerLabel}。按 Opportunity 的年份、领域和 expected_outcomes 改写成自然语言,不得发明年份、事件事实或候选映射。`;
return `引擎给出的区分机会绑定 ${layerLabel}。按 Opportunity 的时间范围、领域和 expected_outcomes 改写成自然语言,不得发明年份、事件事实或候选映射,不得改写时间范围`;
}
function remainingOutcomes(
@@ -113,6 +113,8 @@ export type MethodFollowup = Readonly<{
semantic_key?: string;
candidate_split_hash?: string;
probe_year?: number;
year_label?: string;
probe_month?: number;
choice_kind?: "existence" | "varga_style" | "event_quality";
candidate_ids?: readonly string[];
expected_outcomes?: DiscriminatingEventProbe["expected_outcomes"];
@@ -497,7 +499,13 @@ function followupOwnedProbe(
if (styleOptions.length !== 4) return null;
return {
year: item.probe_year ?? 0,
year_label: item.probe_year ? `${item.probe_year} 年前后` : "当前这几个候选",
year_label: item.year_label
?? (item.probe_year && item.probe_month
? `${item.probe_year}${item.probe_month} 月前后`
: item.probe_year
? `${item.probe_year} 年前后`
: "当前这几个候选"),
...(item.probe_month ? { month: item.probe_month } : {}),
domain: item.domain as EventProbeDomain,
event_family: followupEventFamily(item.domain, kind),
source: "dasha_activation",
@@ -579,7 +587,7 @@ function agentHint(
extra = "",
evidence: readonly MethodFollowupEvidence[] = [],
): string {
return `${why}本题绑定 ${varga}${extra}${recordedKindYearHint(evidence)}点选卡只出 A/B/C/D。用简体中文自己写一句追问;年份和事件家族以 choice_frame.period 与探针为准,不得发明年份,不得改问其他领域,不得把探针年份说成已经发生的事实。不要调用 set-focus。正文不要复述选项。`.replace(/\s+/g, " ").trim();
return `${why}本题绑定 ${varga}${extra}${recordedKindYearHint(evidence)}点选卡只出 A/B/C/D。用简体中文自己写一句追问;时间范围和事件家族以 choice_frame.period 与探针为准,不得发明年份,不得改写时间范围,不得改问其他领域,不得把探针时间说成已经发生的事实。不要调用 set-focus。正文不要复述选项。`.replace(/\s+/g, " ").trim();
}
export function shouldAttachChoiceFrame(
@@ -995,6 +1003,8 @@ export function buildMethodFollowupPlan(input: {
semantic_key: liveProbe.semantic_key,
candidate_split_hash: liveProbe.candidate_split_hash,
probe_year: liveProbe.year,
year_label: liveProbe.year_label,
probe_month: liveProbe.month,
choice_kind: liveProbe.choice_kind,
candidate_ids: liveProbe.candidate_ids ?? candidateIdsFromProbe(liveProbe),
expected_outcomes: liveProbe.expected_outcomes,
@@ -1093,6 +1103,8 @@ export function buildMethodFollowupPlan(input: {
semantic_key: conflictProbe.semantic_key ?? `${conflictProbe.domain}.${conflictProbe.year}`,
candidate_split_hash: conflictProbe.candidate_split_hash,
probe_year: conflictProbe.year,
year_label: conflictProbe.year_label,
probe_month: conflictProbe.month,
choice_kind: conflictProbe.choice_kind ?? "existence",
candidate_ids: conflictProbe.candidate_ids ?? candidateIdsFromProbe(conflictProbe),
expected_outcomes: conflictProbe.expected_outcomes,
@@ -205,6 +205,7 @@ export type ProbeExpectedOutcome = Readonly<{
export type DiscriminatingEventProbe = Readonly<{
year: number;
year_label: string;
month?: number;
domain: EventProbeDomain;
event_family: string;
source: EventProbeSource;
@@ -511,6 +512,9 @@ export function parseEventProbes(
const family = asText(row?.event_family, 80);
const meaning = asText(row?.user_meaning, 160);
const label = asText(row?.year_label, 40);
const month = typeof row?.month === "number" && Number.isInteger(row.month) && row.month >= 1 && row.month <= 12
? row.month
: null;
if (
!row
|| year === null
@@ -536,6 +540,7 @@ export function parseEventProbes(
const probe: DiscriminatingEventProbe = {
year,
year_label: label,
...(month ? { month } : {}),
domain: domain as EventProbeDomain,
event_family: family,
source: source as EventProbeSource,
@@ -128,6 +128,42 @@ test("choice frames ask one biographical event from a server probe, not competin
assert.equal(mergeChoiceCard(frame, null), null);
});
test("choice frames keep the engine month lock instead of collapsing to a year", () => {
const frame = buildChoiceFrame(
{
method_id: "d10_career",
ask_theme: "dated_event",
domain: "career",
user_prompt_hint: "unused",
},
{
evidence: [{
status: "confirmed",
domain: "education",
datePrecision: "year",
occurredFrom: "2016-01-01",
occurredTo: null,
}],
probes: [{
...MOVE_PROBE,
year: 2018,
month: 3,
year_label: "2018 年 3 月前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_boundary",
user_meaning: "时间范围锁定 2018 年 3 月前后;领域锁定 career。",
semantic_key: "career.2018.03.dasha_boundary",
}],
},
);
assert.ok(frame);
assert.equal(frame.period, "2018 年 3 月前后");
assert.match(frame.prompt, /2018 年 3 月前后/);
assert.match(frame.why, /2018 年 3 月前后/);
assert.doesNotMatch(frame.period, /^2018 年前后$/);
});
test("server-owned card names the event family, not a generic 有没有这件事", () => {
const frame = buildChoiceFrame(
{
@@ -74,6 +74,32 @@ test("receipt parser drops invalid distinguish probes and known_event_quality",
assert.ok((parsed[0]?.information_gain ?? 0) > 0);
});
test("receipt parser keeps engine month on dasha boundary probes", () => {
const parsed = parseDiscriminatingEventProbes([{
year: 2018,
month: 3,
year_label: "2018 年 3 月前后",
domain: "career",
event_family: "职责变化",
source: "dasha_boundary",
tracks: ["vimshottari", "narayana"],
tracks_agree: true,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2018 年 3 月前后",
role: "distinguish",
information_gain: 0.4,
semantic_key: "career.2018.03.dasha_boundary",
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"] },
],
}]);
assert.equal(parsed[0]?.month, 3);
assert.equal(parsed[0]?.year_label, "2018 年 3 月前后");
assert.equal(parsed[0]?.source, "dasha_boundary");
});
test("randomized hidden mutated answers change posterior only when mapped", () => {
const probe = probeFromEngine({
year: 2018,
@@ -358,6 +358,30 @@ test("dasha conflict probe jumps after four scoreable events leave three trainin
}), "collect_evidence");
});
test("dasha conflict probe keeps the engine month on the choice card", () => {
const plan = buildMethodFollowupPlan({
evidence: [
datedEvidence("education", "2016"),
datedEvidence("education", "2020"),
datedEvidence("relationship", "2018"),
datedEvidence("family", "2023"),
],
eventProbes: [{
...CAREER_CONFLICT_PROBE,
month: 3,
year_label: "2018 年 3 月前后",
source: "dasha_boundary",
user_meaning: "时间范围锁定 2018 年 3 月前后;领域锁定 career。",
semantic_key: "career.2018.03.dasha_boundary",
}],
});
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(plan.next_followup?.choice_frame?.period, "2018 年 3 月前后");
assert.equal(plan.next_followup?.year_label, "2018 年 3 月前后");
assert.equal(plan.next_followup?.probe_month, 3);
assert.match(plan.next_followup?.user_prompt_hint ?? "", /2018 年 3 月前后/);
});
test("three scoreable events in one domain still rotate methods instead of reverse-inferring", () => {
const plan = buildMethodFollowupPlan({
evidence: [
+3 -1
View File
@@ -202,6 +202,7 @@ def candidate_split_hash(
domain: str,
year: int,
groups: Sequence[Sequence[str]],
month: int | None = None,
) -> str:
version = candidate_set_version_value or candidate_set_version(groups)
grouped = "|".join(
@@ -209,7 +210,8 @@ def candidate_split_hash(
for group in groups
if group
)
payload = f"{version}:{domain}:{year}:{grouped}"
window = f"{year}-{int(month):02d}" if isinstance(month, int) and 1 <= month <= 12 else str(year)
payload = f"{version}:{domain}:{window}:{grouped}"
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
+129 -56
View File
@@ -40,6 +40,7 @@ from scripts.rectification.probe_question_contract import complete_style_options
from scripts.rectification.refinement_packet import match_level
MAX_PROBES = 3
MIN_BOUNDARY_DAYS = 45
LEVEL_RANK = {"none": 0, "weak": 1, "medium": 2, "strong": 3}
LEVEL_P = {"none": 0.15, "weak": 0.35, "medium": 0.62, "strong": 0.82}
SCORING_LAYERS = ("d1", "d9", "d10", "d4", "d5", "d24", "d7", "d12", "d2", "d11", "d30")
@@ -160,10 +161,16 @@ def _event_year(event: dict[str, Any]) -> int | None:
return event_year(event)
def _year_label(year: int) -> str:
def _period_label(year: int, month: int | None = None) -> str:
if isinstance(month, int) and 1 <= month <= 12:
return f"{year}{month} 月前后"
return f"{year} 年前后"
def _year_label(year: int) -> str:
return _period_label(year)
def _age_band_year(birth_year: int, domain: str, today: date) -> int | None:
catalog = DOMAIN_CATALOG.get(domain)
if not catalog:
@@ -361,19 +368,53 @@ def _tracks_present(rule_ids: Sequence[str]) -> tuple[bool, bool]:
)
def _vim_start_years(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[int]:
def _vim_start_dates(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[date]:
nakshatra, progress, _ = dasha_analyzer.lon_to_nakshatra(float(moon_longitude))
timeline, _, _, _ = dasha_analyzer.build_dasha_timeline(birth_date, nakshatra, progress)
years: list[int] = []
starts: list[date] = []
for major in timeline:
start = major.get("start")
if isinstance(start, datetime) and lo <= start.year <= hi:
years.append(start.year)
starts.append(start.date())
for minor in dasha_analyzer.build_antardasha(major):
minor_start = minor.get("start")
if isinstance(minor_start, datetime) and lo <= minor_start.year <= hi:
years.append(minor_start.year)
return years
starts.append(minor_start.date())
return starts
def _vim_start_years(birth_date: str, moon_longitude: float, lo: int, hi: int) -> list[int]:
return [item.year for item in _vim_start_dates(birth_date, moon_longitude, lo, hi)]
def _narayana_start_dates(
ascendant_index: int,
planet_longitudes: dict[str, float],
birth_date: str,
lo: int,
hi: int,
) -> list[date] | None:
periods = narayana_dasha.calc_narayana_mahadasha(ascendant_index, planet_longitudes)
if not periods:
return None
birth = datetime.strptime(birth_date, "%Y-%m-%d")
starts: list[date] = []
for major in periods:
start_age = major.get("start_age")
if not isinstance(start_age, (int, float)):
return None
at = (birth + timedelta(days=float(start_age) * 365.2425)).date()
if lo <= at.year <= hi:
starts.append(at)
antars = narayana_dasha.calc_narayana_antardasha(periods, int(major["sign_idx"]))
for minor in antars:
minor_age = minor.get("start_age")
if not isinstance(minor_age, (int, float)):
continue
minor_at = (birth + timedelta(days=float(minor_age) * 365.2425)).date()
if lo <= minor_at.year <= hi:
starts.append(minor_at)
return starts
def _narayana_start_years(
@@ -383,36 +424,41 @@ def _narayana_start_years(
lo: int,
hi: int,
) -> list[int] | None:
periods = narayana_dasha.calc_narayana_mahadasha(ascendant_index, planet_longitudes)
if not periods:
return None
birth = datetime.strptime(birth_date, "%Y-%m-%d")
years: list[int] = []
for major in periods:
start_age = major.get("start_age")
if not isinstance(start_age, (int, float)):
return None
year = (birth + timedelta(days=float(start_age) * 365.2425)).year
if lo <= year <= hi:
years.append(year)
antars = narayana_dasha.calc_narayana_antardasha(periods, int(major["sign_idx"]))
for minor in antars:
minor_age = minor.get("start_age")
if not isinstance(minor_age, (int, float)):
starts = _narayana_start_dates(ascendant_index, planet_longitudes, birth_date, lo, hi)
return None if starts is None else [item.year for item in starts]
def _as_start_date(value: date | datetime | int) -> date | None:
if isinstance(value, datetime):
return value.date()
if isinstance(value, date):
return value
if isinstance(value, int) and 1900 <= value <= 2100:
return date(value, 7, 1)
return None
def _boundary_windows(left: Sequence[date | datetime | int], right: Sequence[date | datetime | int]) -> list[date]:
windows: list[date] = []
seen: set[tuple[int, int]] = set()
for raw_left, raw_right in zip(left, right):
one = _as_start_date(raw_left)
two = _as_start_date(raw_right)
if one is None or two is None:
continue
if one.year == two.year and abs((one - two).days) < MIN_BOUNDARY_DAYS:
continue
for item in (one, two):
key = (item.year, item.month)
if key in seen:
continue
minor_year = (birth + timedelta(days=float(minor_age) * 365.2425)).year
if lo <= minor_year <= hi:
years.append(minor_year)
return years
seen.add(key)
windows.append(item)
return windows
def _boundary_years(left: list[int], right: list[int]) -> set[int]:
years: set[int] = set()
for one, two in zip(left, right):
if abs(one - two) >= 1:
years.add(one)
years.add(two)
return years
return {item.year for item in _boundary_windows(left, right)}
def _score_year(
@@ -421,6 +467,7 @@ def _score_year(
birth_date: str,
domain: str,
year: int,
month: int | None = None,
) -> dict[str, Any] | None:
catalog = DOMAIN_CATALOG[domain]
prefixes, _ = DOMAIN_CONFIG[domain]
@@ -432,13 +479,14 @@ def _score_year(
moon = (context.get("planet_longitudes") or {}).get("Moon")
if candidate_at is None or not isinstance(moon, (int, float)):
return None
event_at = datetime(year, 7, 1)
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
event_at = datetime(year, month_value, 15) if month_value else datetime(year, 7, 1)
event = {
"id": f"probe-{domain}-{year}",
"id": f"probe-{domain}-{year}" + (f"-{month_value:02d}" if month_value else ""),
"domain": domain,
"event_kind": catalog["kind"],
"date": f"{year}-07-01",
"precision": "year",
"date": f"{year}-{month_value:02d}-15" if month_value else f"{year}-07-01",
"precision": "month" if month_value else "year",
"summary": catalog["event_family"],
}
try:
@@ -532,6 +580,7 @@ def _public_probe(
tracks_agree: bool,
user_meaning: str,
event_family: str,
month: int | None = None,
**extra: Any,
) -> dict[str, Any]:
if source == "known_event_quality":
@@ -543,9 +592,10 @@ def _public_probe(
else:
phase = PROBE_PHASE_CANDIDATE_DISCRIMINATOR
role = "distinguish"
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
payload = {
"year": year,
"year_label": _year_label(year),
"year_label": _period_label(year, month_value),
"domain": domain,
"event_family": event_family,
"source": source,
@@ -555,13 +605,15 @@ def _public_probe(
"user_meaning": user_meaning,
"role": role,
"phase": phase,
"semantic_key": f"{domain}.{year}",
"semantic_key": f"{domain}.{year}.{month_value:02d}" if month_value else f"{domain}.{year}",
"information_gain": 0.0,
"candidate_split_hash": f"{domain}:{year}",
"candidate_split_hash": f"{domain}:{year}" + (f"-{month_value:02d}" if month_value else ""),
"expected_outcomes": [],
"candidate_ids": [],
"choice_kind": "event_quality" if source == "known_event_quality" else "existence",
}
if month_value:
payload["month"] = month_value
payload.update(extra)
if payload["role"] == "distinguish":
payload["candidate_ids"] = candidate_ids_from_outcomes(payload.get("expected_outcomes") or [])
@@ -661,15 +713,23 @@ def _evaluate_contexts(
domain: str,
year: int,
source: str,
month: int | None = None,
clusters: Sequence[dict[str, Any]] | None = None,
set_version: str | None = None,
) -> dict[str, Any] | None:
scored_rows: list[tuple[str, list[str]]] = []
month_value = month if isinstance(month, int) and 1 <= month <= 12 else None
for context in contexts:
time = _context_time(context)
if not time:
continue
scored = _score_year(context, birth_date=birth_date, domain=domain, year=year)
scored = _score_year(
context,
birth_date=birth_date,
domain=domain,
year=year,
month=month_value,
)
if scored is None:
continue
scored_rows.append((time, list(scored.get("rule_ids") or [])))
@@ -729,22 +789,25 @@ def _evaluate_contexts(
candidate_set_version_value=version,
domain=domain,
year=year,
month=month_value,
groups=[yes_times, no_times],
)
period = _period_label(year, month_value)
probe = _public_probe(
year=year,
month=month_value,
domain=domain,
source=source,
tracks=("vimshottari", "narayana"),
tracks_agree=vim_hit and narayana_hit,
user_meaning=_agent_brief(
year_label=_year_label(year),
year_label=period,
domain=domain,
family=str(DOMAIN_CATALOG[domain]["event_family"]),
),
event_family=str(DOMAIN_CATALOG[domain]["event_family"]),
information_gain=gain,
semantic_key=f"{domain}.{year}.{source}",
semantic_key=f"{domain}.{year}.{month_value:02d}.{source}" if month_value else f"{domain}.{year}.{source}",
candidate_split_hash=split,
candidate_set_version=version,
expected_outcomes=outcomes,
@@ -904,15 +967,24 @@ def discriminating_event_probes(
left, right = reps[0], reps[-1]
left_moon = float(left["planet_longitudes"]["Moon"])
right_moon = float(right["planet_longitudes"]["Moon"])
vim_years = _boundary_years(
_vim_start_years(birth_date, left_moon, lo, hi),
_vim_start_years(birth_date, right_moon, lo, hi),
vim_windows = _boundary_windows(
_vim_start_dates(birth_date, left_moon, lo, hi),
_vim_start_dates(birth_date, right_moon, lo, hi),
)
left_narayana = _narayana_start_years(int(left["ascendant_index"]), left["planet_longitudes"], birth_date, lo, hi)
right_narayana = _narayana_start_years(int(right["ascendant_index"]), right["planet_longitudes"], birth_date, lo, hi)
narayana_years: set[int] = set()
left_narayana = _narayana_start_dates(int(left["ascendant_index"]), left["planet_longitudes"], birth_date, lo, hi)
right_narayana = _narayana_start_dates(int(right["ascendant_index"]), right["planet_longitudes"], birth_date, lo, hi)
narayana_windows: list[date] = []
if left_narayana is not None and right_narayana is not None:
narayana_years = _boundary_years(left_narayana, right_narayana)
narayana_windows = _boundary_windows(left_narayana, right_narayana)
boundary_dates: list[date] = []
seen_windows: set[tuple[int, int]] = set()
for item in [*vim_windows, *narayana_windows]:
key = (item.year, item.month)
if key in seen_windows:
continue
seen_windows.add(key)
boundary_dates.append(item)
boundary_dates.sort()
probes: list[dict[str, Any]] = []
for domain in domains:
if domain not in DOMAIN_CATALOG:
@@ -920,18 +992,19 @@ def discriminating_event_probes(
known_years = _event_years(events, domain)
blocked_years = _existence_blocked_years(domain, known_years)
domain_lo = max(lo, birth_year + int(DOMAIN_CATALOG[domain]["age_lo"])) if domain == "relationship" else lo
boundary = sorted(year for year in vim_years | narayana_years if domain_lo <= year <= hi)
boundary = [item for item in boundary_dates if domain_lo <= item.year <= hi]
best = None
for year in boundary:
if year in blocked_years:
for at in boundary:
if at.year in blocked_years:
continue
if f"{domain}:{year}" in holdout_keys:
if f"{domain}:{at.year}" in holdout_keys:
continue
found = _evaluate_contexts(
reps,
birth_date=birth_date,
domain=domain,
year=year,
year=at.year,
month=at.month,
source="dasha_boundary",
clusters=clusters,
set_version=set_version,
@@ -956,13 +1029,13 @@ def discriminating_event_probes(
probes.append(best)
probes.sort(key=lambda row: (-float(row.get("information_gain") or 0), str(row.get("semantic_key") or "")))
public: list[dict[str, Any]] = []
seen: set[tuple[str, int, str]] = set()
seen: set[tuple[str, int, int, str]] = set()
for row in probes:
if row.get("source") == "known_event_quality" or row.get("phase") != PROBE_PHASE_CANDIDATE_DISCRIMINATOR:
continue
if distinguish_contract_errors(row):
continue
key = (str(row["domain"]), int(row["year"]), str(row["source"]))
key = (str(row["domain"]), int(row["year"]), int(row.get("month") or 0), str(row["source"]))
encoded = str(row)
if key in seen or "points" in encoded:
continue
@@ -75,7 +75,7 @@ description: "生时校正专用 SkillV10)。以服务器权威 Case、Conv
`CaseConversationSummary` 是长会话的权威记忆,至少投影:confirmed evidence summary、pending revisions、active focus、declined/skipped topics、candidate divergence summary、missing evidence categories、`method_followup_plan`、last result policy。
- 选择下一动作、识别已确认事实、避免重复追问、理解候选差异与结果政策时,优先依据服务器提供的 `CaseConversationSummary``method_followup_plan`
- 不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,仍可计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup``choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative``next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
- 不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,仍可计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup``choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份,不得改写时间范围;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative``next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
- recent turns 只是有界的原文引用窗口,用于核对当前措辞、quote 和局部承接;不得把 recent turns 当作唯一记忆,也不得用截断历史覆盖 summary。
- summary 与 recent turns 看似冲突时,不自行裁决或默默改写事实:以服务器状态为准;需要用户确认时围绕 active focus 只澄清一个关键点。
- 超过长会话窗口后仍不得忘记已确认证据、pending revision、拒答主题或 active focus。
@@ -1,6 +1,6 @@
# Conversation StrategyV10
生时校正访谈按 skill 路径 C:先用自然语言收集带大概年份的经历,再在候选已经分不开时由服务器锁定年份和事件家族,由你写成一句具体生平题干(某年是否搬过家、高考是否发挥失常),用 A/B/C/D 点选卡回答同一件事的吻合程度;不是 10–15 条事件长表,也不是无结构闲聊,更不是让用户给两套盘排序。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。
生时校正访谈按 skill 路径 C:先用自然语言收集带大概年份的经历,再在候选已经分不开时由服务器锁定时间范围和事件家族,由你写成一句具体生平题干(某年或某月是否搬过家、高考是否发挥失常),用 A/B/C/D 点选卡回答同一件事的吻合程度;不是 10–15 条事件长表,也不是无结构闲聊,更不是让用户给两套盘排序。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。
## 1. 每轮上下文优先级
@@ -23,7 +23,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什
- 至多一个主问题;开场可以零问题。
- 不固定复述身份、流程、领域列表、证据数量要求或 opening brief 原文。
示例方向(不是固定话术):“可以先说一件你记得大概年份的事,比如升学、考试或第一份工作。” 区分阶段的题干由你写成自然语言;年份和事件家族以服务器探针为准,不得发明年份。例如把锁定的 2015 年和搬家写成“2015 年前后你是否搬过家?”,把已有高考经历写成“高考的时候是否发挥失常?”
示例方向(不是固定话术):“可以先说一件你记得大概年份的事,比如升学、考试或第一份工作。” 区分阶段的题干由你写成自然语言;时间范围和事件家族以服务器探针为准,不得发明年份,不得改写时间范围。例如把锁定的 2015 年和搬家写成“2015 年前后你是否搬过家?”,把锁定的 2018 年 3 月写成“2018 年 3 月前后你是否入职或职责加重?”,把已有高考经历写成“高考的时候是否发挥失常?”
## 3. 一轮的基本形态
@@ -73,7 +73,7 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服
追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级:
1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;年份和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 年界差或同年激活差,没有可问时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative``next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;时间范围和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 大运/副运起点的年或月差,没有可问边界时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份,不得改写时间范围。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative``next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察用于选题,并在出牌轮写入类型对照(校时方法,不是命运承诺)。
4. pending revision 的一个关键歧义。
5. 已有证据的必要稳定性补强。
@@ -75,7 +75,7 @@ description: "生时校正专用 SkillV10)。以服务器权威 Case、Conv
`CaseConversationSummary` 是长会话的权威记忆,至少投影:confirmed evidence summary、pending revisions、active focus、declined/skipped topics、candidate divergence summary、missing evidence categories、`method_followup_plan`、last result policy。
- 选择下一动作、识别已确认事实、避免重复追问、理解候选差异与结果政策时,优先依据服务器提供的 `CaseConversationSummary``method_followup_plan`
- 不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,仍可计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup``choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative``next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
- 不要按 `missing_evidence_categories` 轮询迁居。财务与健康只有用户主动说才问,仍可计分。下一问只跟 `method_followup_plan.next_followup`。先走完方法覆盖(感情 → 事业 → 家人 → 职业 → 占问),再对已覆盖领域做精度追问。已有带日期事件且存在 `discriminating_event_probes` 大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层,不要 offer。占问不挡出牌;职业挡出牌。外貌、体质、胎记或疤痕不得追问。收集经历用自然语言问一件带大概年份的事,set-focus 不要写 choice。只有 `next_followup``choice_frame`(冲突探针、候选已经分不开或采用后核对前事)时才写 A/B/C/D 点选卡;题干由你写成自然语言,时间范围、领域和语义目标以服务器探针为准,不得发明年份,不得改写时间范围;不要逐字复述服务器的事件家族标签,也不要把标签里的多个例子全堆进一句。结合最近对话只选一个用户最容易回答的口语入口,不要问两套盘哪个更像。正文不要复述选项。「先这样」由服务器补全。`next_user_action.id=adopt_representative``next_followup` 为空,本轮零追问。`next_user_action.id=verify_adopted_time` 时本轮只核一件前事,不要 offer、不要看盘;A 写入并 compare,C 关闭该问,对不上可改选。`id=start_consultation` 时请用户用当前采用时间看盘。`deferred_followup` 留给用户以后再补,不得当成本轮问题。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问,不得 offer。
- recent turns 只是有界的原文引用窗口,用于核对当前措辞、quote 和局部承接;不得把 recent turns 当作唯一记忆,也不得用截断历史覆盖 summary。
- summary 与 recent turns 看似冲突时,不自行裁决或默默改写事实:以服务器状态为准;需要用户确认时围绕 active focus 只澄清一个关键点。
- 超过长会话窗口后仍不得忘记已确认证据、pending revision、拒答主题或 active focus。
@@ -1,6 +1,6 @@
# Conversation StrategyV10
生时校正访谈按 skill 路径 C:先用自然语言收集带大概年份的经历,再在候选已经分不开时由服务器锁定年份和事件家族,由你写成一句具体生平题干(某年是否搬过家、高考是否发挥失常),用 A/B/C/D 点选卡回答同一件事的吻合程度;不是 10–15 条事件长表,也不是无结构闲聊,更不是让用户给两套盘排序。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。
生时校正访谈按 skill 路径 C:先用自然语言收集带大概年份的经历,再在候选已经分不开时由服务器锁定时间范围和事件家族,由你写成一句具体生平题干(某年或某月是否搬过家、高考是否发挥失常),用 A/B/C/D 点选卡回答同一件事的吻合程度;不是 10–15 条事件长表,也不是无结构闲聊,更不是让用户给两套盘排序。服务器持有事实、状态、权限、焦点与长会话记忆;Agent 负责意图理解、把问卷说清楚、并选择一个有信息增益的下一步。
## 1. 每轮上下文优先级
@@ -23,7 +23,7 @@ recent turns 不是权威记忆,不得依赖“上一条 assistant 问了什
- 至多一个主问题;开场可以零问题。
- 不固定复述身份、流程、领域列表、证据数量要求或 opening brief 原文。
示例方向(不是固定话术):“可以先说一件你记得大概年份的事,比如升学、考试或第一份工作。” 区分阶段的题干由你写成自然语言;年份和事件家族以服务器探针为准,不得发明年份。例如把锁定的 2015 年和搬家写成“2015 年前后你是否搬过家?”,把已有高考经历写成“高考的时候是否发挥失常?”
示例方向(不是固定话术):“可以先说一件你记得大概年份的事,比如升学、考试或第一份工作。” 区分阶段的题干由你写成自然语言;时间范围和事件家族以服务器探针为准,不得发明年份,不得改写时间范围。例如把锁定的 2015 年和搬家写成“2015 年前后你是否搬过家?”,把锁定的 2018 年 3 月写成“2018 年 3 月前后你是否入职或职责加重?”,把已有高考经历写成“高考的时候是否发挥失常?”
## 3. 一轮的基本形态
@@ -73,7 +73,7 @@ active `ConversationFocus` 是承接型意图的唯一目标来源。它由服
追问必须能澄清事实、提高真实日期精度、补足必要方法层或区分候选;否则不提。优先级:
1. 服务器 `CaseConversationSummary.active focus` 指定的唯一目标。
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;年份和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 年界差或同年激活差,没有可问时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative``next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
2. `method_followup_plan.next_followup` 指定的下一方法层。方法覆盖优先于对已覆盖领域的精度追问:有日期事件 → 感情 → 事业 → 家人(D12/D7/D3)→ 职业(挡出牌,独立于带日期事业事件)→ 占问(只问一次,不挡出牌)→ 再按精度阶段问关系盘/事业盘/居所(D4)/学业成就(D5,D24 换升并入同一问)。已有带日期事件且服务器给出大运冲突探针时,先问该前事筛窗,`source=event_probe` 挡住出牌,不要继续轮询方法层。迁居不进领域轮询,只在 `d4_refine` 精度阶段问搬家/住处。财务与健康只有用户主动说才问,仍可计分。不得询问外貌、体质、胎记或疤痕。收集经历用自然语言。只有候选已经分不开、冲突探针或采用后核对前事时,`choice_frame` 才提供冲突节点;时间范围和事件家族由服务器 `discriminating_event_probes` 锁定(Vimshottari+Narayana 大运/副运起点的年或月差,没有可问边界时才用出生年+年龄带)。题干和 A/B/C/D 由你写成自然语言,A/B 是同一件事的吻合程度,不要照抄 hint,不要问两套盘哪个更像或可能性高低,不得发明年份,不得改写时间范围。Nakshatra pada / Hora / Ghati / Bhava / Pranapada / KP 子主换升只展示,不阻断采用。`next_user_action.id=adopt_representative``next_followup` 为空,不得把 `deferred_followup` 当成本轮问题。`id=verify_adopted_time` 时本轮只核一件前事。仍有挡住出牌的 `next_followup` 时即使 `selection_allowed` 也继续问。
3. candidate divergence / `internal_observations` 显示真正能区分候选的主题。D9/D10 观察用于选题,并在出牌轮写入类型对照(校时方法,不是命运承诺)。
4. pending revision 的一个关键歧义。
5. 已有证据的必要稳定性补强。
+97 -16
View File
@@ -287,6 +287,87 @@ class EventProbesTest(unittest.TestCase):
probes = _probes(_request(), built, ["05:13", "05:40"], "05:13", precision_current="d4_refine")
self.assertTrue(all(item["source"] != "dasha_boundary" for item in probes))
def test_same_year_month_apart_boundary_uses_engine_month(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, 3, 15)] if moon <= 100.0 else [date(2018, 9, 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, 3, 15)] if moon <= 100.0 else [date(2018, 9, 20)]
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
del birth_date, domain
early = probes_mod._context_time(context) == "05:13"
if year == 2018 and month == 3:
return {"rule_ids": ["vim_ad_domain_lord"] if early else ["no_domain_activation"]}
if year == 2018 and month == 9:
return {"rule_ids": ["vim_md_domain_house"] if early else ["no_domain_activation"]}
return {"rule_ids": ["no_domain_activation"]}
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")
row = next(item for item in probes if item["domain"] == "relocation")
self.assertEqual(row["source"], "dasha_boundary")
self.assertEqual(row["year"], 2018)
self.assertIn(row["month"], {3, 9})
self.assertRegex(row["year_label"], r"2018 年 [39] 月前后")
self.assertIn("2018 年", row["user_meaning"])
self.assertIn("月前后", row["user_meaning"])
self.assertNotIn("points", str(row))
def test_vim_start_dates_keep_engine_month(self) -> None:
from scripts.rectification import event_probes as probes_mod
starts = probes_mod._vim_start_dates("1997-08-08", 100.0, 2005, 2025)
self.assertTrue(starts)
self.assertTrue(all(isinstance(item, date) and not isinstance(item, datetime) for item in starts))
self.assertTrue(any(item.month != 7 or item.day != 1 for item in starts))
def test_age_band_fallback_stays_year_precision(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=100.0),
]
}
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
del birth_date, domain, year, month
return {
"rule_ids": ["vim_md_domain_house"]
if probes_mod._context_time(context) == "05:13"
else ["no_domain_activation"]
}
with (
patch.object(probes_mod, "_vim_start_dates", return_value=[]),
patch.object(probes_mod, "_narayana_start_dates", return_value=[]),
patch.object(probes_mod, "_score_year", side_effect=fake_score),
):
probes = _probes(_request(), built, ["05:13", "05:40"], "05:13")
row = next(item for item in probes if item["source"] == "dasha_activation")
self.assertNotIn("month", row)
self.assertRegex(row["year_label"], r"^\d{4} 年前后$")
def test_missing_narayana_inputs_do_not_claim_dasha_year(self) -> None:
built = {
"static_contexts": [
@@ -402,15 +483,15 @@ class EventProbesTest(unittest.TestCase):
]
}
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[int]:
return [2010, 2020] if moon <= 100.0 else [2009, 2019]
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
return [date(2010, 3, 15), date(2020, 3, 15)] if moon <= 100.0 else [date(2009, 9, 15), date(2019, 9, 15)]
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[int]:
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
moon = float(planets.get("Moon") or 0)
return [2010, 2020] if moon <= 100.0 else [2009, 2019]
return [date(2010, 3, 15), date(2020, 3, 15)] if moon <= 100.0 else [date(2009, 9, 15), date(2019, 9, 15)]
def fake_score(context: dict, *, birth_date: str, domain: str, year: int) -> dict:
del birth_date, domain
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
del birth_date, domain, month
early = probes_mod._context_time(context) == "05:13"
if year == 2010:
return {"rule_ids": ["vim_ad_domain_lord"] if early else ["no_domain_activation"]}
@@ -419,8 +500,8 @@ class EventProbesTest(unittest.TestCase):
return {"rule_ids": ["no_domain_activation"]}
with (
patch.object(probes_mod, "_vim_start_years", side_effect=fake_vim),
patch.object(probes_mod, "_narayana_start_years", side_effect=fake_narayana),
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")
@@ -441,15 +522,15 @@ class EventProbesTest(unittest.TestCase):
]
}
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[int]:
return [2003, 2019] if moon <= 100.0 else [2002, 2018]
def fake_vim(_birth_date: str, moon: float, _lo: int, _hi: int) -> list[date]:
return [date(2003, 3, 15), date(2019, 3, 15)] if moon <= 100.0 else [date(2002, 9, 15), date(2018, 9, 15)]
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[int]:
def fake_narayana(_asc: int, planets: dict, _birth_date: str, _lo: int, _hi: int) -> list[date]:
moon = float(planets.get("Moon") or 0)
return [2003, 2019] if moon <= 100.0 else [2002, 2018]
return [date(2003, 3, 15), date(2019, 3, 15)] if moon <= 100.0 else [date(2002, 9, 15), date(2018, 9, 15)]
def fake_score(context: dict, *, birth_date: str, domain: str, year: int) -> dict:
del birth_date, domain, year
def fake_score(context: dict, *, birth_date: str, domain: str, year: int, month: int | None = None) -> dict:
del birth_date, domain, year, month
return {
"rule_ids": ["vim_md_domain_house"]
if probes_mod._context_time(context) == "05:13"
@@ -457,8 +538,8 @@ class EventProbesTest(unittest.TestCase):
}
with (
patch.object(probes_mod, "_vim_start_years", side_effect=fake_vim),
patch.object(probes_mod, "_narayana_start_years", side_effect=fake_narayana),
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")