fix(rectification): ask reverse-inference probes at engine dasha months
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:
@@ -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: [
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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: "生时校正专用 Skill(V10)。以服务器权威 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 Strategy(V10)
|
||||
|
||||
生时校正访谈按 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: "生时校正专用 Skill(V10)。以服务器权威 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。
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Conversation Strategy(V10)
|
||||
|
||||
生时校正访谈按 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. 已有证据的必要稳定性补强。
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user