diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md
index 1eeb1511..b09164e3 100644
--- a/frontend/docs/VOICE.md
+++ b/frontend/docs/VOICE.md
@@ -290,7 +290,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
| 生成中输入框变灰,回车没反应 | 生成中仍可打字;回车后显示「已排队,回答结束后发出」,可撤回 | 对标主流对话产品,焦点不丢。 |
| 停止后出现红色告警条 | 已停止,已生成的内容保留;本次不会扣点。 | 停止是用户动作,不是出错。 |
| 打字回答发出后一直是「正在处理… / 正在分析…」,半分钟没有变化 | 发出即「收到,正在对照你的档案…」,随后按阶段换成「正在记下这件事…」「正在重新对照盘面…」「正在准备下一个问题…」。 | 发出后 300 ms 内要有一句确定的话;阶段由真实动作推动,只在活动行里,正文与历史里都没有(BUG-1047)。 |
-| 也可以再说一件你记得大概时间的事。 / 还差带月份的经历,领域不限。 | 训练门开后给区间卡,并写「如果还记得……范围还能再收一截」。材料不够时只写记下了哪几件、再来一件不是这类的、至少两个具体例子;输入框占位「再说一件带年月的事」。 | 训练门未开时永远给结果或精确缺口;不说「领域不限」「做不了」。交付后采集线关完的邀请见下一行。 |
+| 也可以再说一件你记得大概时间的事。 / 还差带月份的经历,领域不限。 | 训练门开后给区间卡,并写「如果还记得……范围还能再收一截」。材料不够时只写记下了哪几件、还差几件与还缺几个方面(门槛 4 件、3 个方面,2026-10-02 R1;例「再来两件记得大概年月的事,其中至少一件不是上学或工作的,就能开始筛」,只差一件时仍写「再来一件……」)、至少两个具体例子;输入框占位「再说一件带年月的事」。 | 训练门未开时永远给结果或精确缺口;不说「领域不限」「做不了」。交付后采集线关完的邀请见下一行。 |
| 能问的都问完了 / 你要是还记得确切哪一天的事,不限领域,说出来我接着算 | 还有题可问时:「现在还剩 04:51–05:11 里 5 个候选,再对照几件经历会更准」,下面是系统点名的题。答「有」之后是口述「大概哪年几月?」,在输入框打字回答,不再出「哪一类事 + 发生年月」的卡。七条线和跳过线的重问问完了(按时间段点名的引导题一次校正最多两道),或用户说「没有了」,就出目前范围卡。 | 有题就继续引导;年月阶段直接打字。没题了照常给结果,不写「未达门槛」,也不为凑门槛再追问。不承诺「再补几件就能定到分钟」。 |
| 2018 年 3 到 5 月之间,有没有入职、换工作或职责变重?(引擎其实不知道是哪一类) | 「2018 年 3 到 5 月之间,有没有什么事,比如搬家或开始长期住外地、开始认真关系、分手或结婚?」只有 D9 / D10 那两条轨道才点名领域。 | 边界只说得出「哪段时间」,说不出「哪一类事」。例子最多三个,只列用户还没拒答、也还没说过的领域。 |
| (录入卡)哪一类事 + 发生年月 | 答「有」后只剩口述「大概哪年几月?」,在输入框打字。 | 多余入口删除;年月阶段不再有第二套录入。账本仍记用户原话,不得复用题干例子列表。 |
diff --git a/frontend/src/components/birth-time-life-events.tsx b/frontend/src/components/birth-time-life-events.tsx
index 5c58d49a..4fe24f41 100644
--- a/frontend/src/components/birth-time-life-events.tsx
+++ b/frontend/src/components/birth-time-life-events.tsx
@@ -4,6 +4,7 @@ import { useRef, useState } from "react";
import { lifeEventSchema } from "@/lib/birth-time-journey";
import type { LifeEvent } from "@/lib/birth-time-evidence";
import { ErrorToast } from "@/components/error-toast";
+import { RECTIFICATION_POLICY } from "@/lib/rectification-policy";
type EventDraft = {
readonly rowId: string;
@@ -27,7 +28,7 @@ const domainOptions = [
{ value: "health_pressure", label: "健康、事故或低谷" },
] as const;
-const initialDomains = ["education", "career", "relationship"] as const;
+const initialDomains = ["education", "career", "relationship", "relocation"] as const;
function initialDrafts(events: readonly LifeEvent[]): readonly EventDraft[] {
if (events.length > 0) {
@@ -107,7 +108,7 @@ export function BirthTimeLifeEvents({
}
function removeDraft(rowId: string) {
- setDrafts((current) => current.length <= 3
+ setDrafts((current) => current.length <= RECTIFICATION_POLICY.minConfirmationEvents
? current
: current.filter((draft) => draft.rowId !== rowId));
}
@@ -124,8 +125,8 @@ export function BirthTimeLifeEvents({
return;
}
const events = parsed.flatMap((item) => item.success ? [item.data] : []);
- if (new Set(events.map((event) => event.domain)).size < 2) {
- setError("请至少选择两个不同领域的经历,以便区分候选时间。");
+ if (new Set(events.map((event) => event.domain)).size < RECTIFICATION_POLICY.minConfirmationDomains) {
+ setError("请至少选择三个不同领域的经历,以便区分候选时间。");
return;
}
setError("");
@@ -139,7 +140,7 @@ export function BirthTimeLifeEvents({
{drafts.length} / 6
- 请填写至少三条、覆盖两个领域的经历。只使用日期和类型评分,不会从描述中猜测。
+ 请填写至少四条、覆盖三个领域的经历。只使用日期和类型评分,不会从描述中猜测。
{drafts.map((draft, index) => (
diff --git a/frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts b/frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts
index 188ae25d..13969c26 100644
--- a/frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts
+++ b/frontend/src/lib/rectification-agentic/core/convergence-evaluator.ts
@@ -4,7 +4,8 @@ import {
CONVERGENCE_LEAD,
CONVERGENCE_TOP_SHARE,
DEFAULT_MAX_DISCRIMINATION_ROUNDS,
- MIN_TRAINING_EVENTS,
+ MIN_DATED_DOMAINS,
+ MIN_DATED_EVENTS,
STABLE_WINNER_ROUNDS,
type InferenceCandidate,
type InferenceEvent,
@@ -42,7 +43,8 @@ export function evaluateConvergence(state: Pick<
const active = rankActive(state.candidates);
const top = active[0] ?? null;
const runnerUp = active[1] ?? null;
- const trainingCount = state.events.filter((item) => item.usage === "training").length;
+ const dated = state.events.filter((item) => item.usage === "training" || item.usage === "holdout");
+ const datedDomains = new Set(dated.map((item) => item.domain));
const unionRange = unionStillValidRange(state.candidates) ?? state.credible_range;
const equivalent = Boolean(
unionRange
@@ -64,7 +66,7 @@ export function evaluateConvergence(state: Pick<
const stable = winnerStable(state.rounds, top?.id ?? null);
const noCriticalConflict = (top?.strong_conflict_count ?? 0) === 0;
const holdoutPassed = state.holdout_passed;
- const sufficient = trainingCount >= MIN_TRAINING_EVENTS;
+ const sufficient = dated.length >= MIN_DATED_EVENTS && datedDomains.size >= MIN_DATED_DOMAINS;
if (equivalent) {
return {
diff --git a/frontend/src/lib/rectification-agentic/core/rectification-decision.ts b/frontend/src/lib/rectification-agentic/core/rectification-decision.ts
index a9663376..b1182fbe 100644
--- a/frontend/src/lib/rectification-agentic/core/rectification-decision.ts
+++ b/frontend/src/lib/rectification-agentic/core/rectification-decision.ts
@@ -18,6 +18,8 @@ import { EFFECTIVE_ANSWER_SAFETY_CAP } from "../../birth-time-dynamic-stop-polic
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
import {
DEFAULT_MAX_DISCRIMINATION_ROUNDS,
+ MIN_DATED_DOMAINS,
+ MIN_DATED_EVENTS,
type RectificationPhase,
type ResultStatus,
} from "./types.ts";
@@ -105,9 +107,9 @@ export function engineCapabilityCeilingFromReceipt(value: unknown): EngineCapabi
export const RECTIFICATION_TERMINATION_COPY = "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。";
-/** Standalone range-delivery floor; the exact-minute confirmation gate remains 4/3. */
-export const MIN_STANDALONE_DATED_EVENTS = 3;
-export const MIN_STANDALONE_DATED_DOMAINS = 2;
+/** Standalone range-delivery floor (R1: 4 dated events / 3 domains); same source as the confirmation gate. */
+export const MIN_STANDALONE_DATED_EVENTS = MIN_DATED_EVENTS;
+export const MIN_STANDALONE_DATED_DOMAINS = MIN_DATED_DOMAINS;
export type DecisionSessionOutcome =
| "collect_evidence"
diff --git a/frontend/src/lib/rectification-agentic/core/types.ts b/frontend/src/lib/rectification-agentic/core/types.ts
index 9ac1823e..00dd91bb 100644
--- a/frontend/src/lib/rectification-agentic/core/types.ts
+++ b/frontend/src/lib/rectification-agentic/core/types.ts
@@ -5,13 +5,22 @@
* probe, assign scores, eliminate candidates, or declare convergence.
*/
+import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
+
export const INFERENCE_ALGORITHM_VERSION = "rectification-inference-v1";
export const HIGH_INFORMATION_GAIN = 0.08;
export const DEFAULT_MAX_DISCRIMINATION_ROUNDS = 8;
export const CONVERGENCE_LEAD = 0.2;
export const CONVERGENCE_TOP_SHARE = 0.7;
export const STABLE_WINNER_ROUNDS = 2;
-export const MIN_TRAINING_EVENTS = 3;
+/**
+ * Range-delivery evidence floor (R1, 2026-10-02, BUG-1193): at least 4 dated,
+ * primary-scoreable events across 3 domains, counted on all of them (training
+ * plus the reserved holdout). The only source is
+ * `references/rectification_policy.v1.json`; Python reads the same file.
+ */
+export const MIN_DATED_EVENTS: number = RECTIFICATION_POLICY.minConfirmationEvents;
+export const MIN_DATED_DOMAINS: number = RECTIFICATION_POLICY.minConfirmationDomains;
export const STRONG_CONFLICT_ELIMINATION_COUNT = 3;
export type RectificationPhase =
diff --git a/frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts b/frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts
index 2d818114..a4bf7434 100644
--- a/frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts
+++ b/frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts
@@ -6,7 +6,12 @@
import { rangeDeliveryCollectClosed, USER_COLLECT_QUESTION } from "../user-copy.ts";
import { canonicalCollectDomain } from "./domain-alias.ts";
-import { trainingScoreableGate } from "./evidence-model.ts";
+import {
+ isPrimaryScoreableEvidence,
+ MIN_ACCEPTANCE_DOMAINS,
+ MIN_ACCEPTANCE_EVENTS,
+ trainingScoreableGate,
+} from "./evidence-model.ts";
import { hasDatedTransition, signFromTransitions, validDatedTransitions, type TransitionSignLookup } from "../core/sign-from-transitions.ts";
export const COLLECT_KIND_ORDER = [
@@ -459,10 +464,39 @@ export function preciseGapNarration(
: covered.size > 1
? ""
: "";
- const needOther = covered.size === 1
- ? `再来一件不是${KIND_LABEL[[...covered][0]]}的、记得大概年月的事就能开始筛,比如${exampleText}。`
- : `再来一件记得大概年月的事就能开始筛,比如${exampleText}。`;
- return `${recorded}${sameKind}${needOther}`;
+ return `${recorded}${sameKind}${evidenceGapAsk(evidence, covered)}比如${exampleText}。`;
+}
+
+const COUNT_WORD = ["零", "一", "两", "三", "四", "五", "六"] as const;
+
+function countWord(value: number): string {
+ return COUNT_WORD[value] ?? String(value);
+}
+
+/**
+ * R1 (BUG-1193): name the exact gap to the 4-event / 3-domain floor instead
+ * of always asking for "one more". Counts follow trainingScoreableGate.
+ */
+function evidenceGapAsk(
+ evidence: readonly CollectionEvidence[],
+ covered: ReadonlySet
,
+): string {
+ const primary = evidence.filter(isPrimaryScoreableEvidence);
+ const domains = new Set(primary.map((item) => item.domain));
+ const missingEvents = Math.max(0, MIN_ACCEPTANCE_EVENTS - primary.length);
+ const missingDomains = Math.max(0, MIN_ACCEPTANCE_DOMAINS - domains.size);
+ const total = Math.max(1, missingEvents, missingDomains);
+ const labels = [...covered].map((kind) => KIND_LABEL[kind]).join("或");
+ if (missingDomains === 0 || !labels) {
+ const spread = missingDomains > 1 ? `、至少涉及${countWord(missingDomains)}个方面` : "";
+ return `再来${countWord(total)}件记得大概年月的事${spread}${spread ? "," : ""}就能开始筛,`;
+ }
+ if (missingDomains === total) {
+ return total === 1
+ ? `再来一件不是${labels}的、记得大概年月的事就能开始筛,`
+ : `再来${countWord(total)}件不是${labels}、彼此也不同类的、记得大概年月的事就能开始筛,`;
+ }
+ return `再来${countWord(total)}件记得大概年月的事,其中至少${countWord(missingDomains)}件不是${labels}的,就能开始筛,`;
}
export function moreCollectHint(
diff --git a/frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts b/frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts
index 7b39c7b5..1e02ba0c 100644
--- a/frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts
+++ b/frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts
@@ -813,7 +813,7 @@ function evidenceStopInputs(evidence: DecisionDossier["evidence"]): {
const primary = evidence.filter(isPrimaryScoreableEvidence);
return {
// The standalone delivery floor is evaluated by decideRectification;
- // trainingScoreableGate is a separate 3/2 engine-readiness check.
+ // trainingScoreableGate applies the same 4/3 floor (R1) for engine readiness.
datedEventCount: primary.length,
datedDomainCount: new Set(primary.map((item) => item.domain)).size,
};
@@ -1105,8 +1105,9 @@ export function decideAfterInferenceChange(input: {
birthTimeSource: normalizeBirthTimeSource(input.dossier.case.birthTimeSource),
};
}
- const training = input.state.events.filter((item) => item.usage === "training");
- const trainingDomains = new Set(training.map((item) => item.domain));
+ // R1: the floor counts every dated event (training + reserved holdout), like the Python receipt.
+ const dated = input.state.events.filter((item) => item.usage === "training" || item.usage === "holdout");
+ const datedDomains = new Set(dated.map((item) => item.domain));
const evidenceStops = evidenceStopInputs(input.dossier.evidence);
const userUncertaintyHigh = uncertaintyHighFromAnswers(input.state.answered_probes);
const contrastPacket = contrastPacketFromState(input.state);
@@ -1147,8 +1148,8 @@ export function decideAfterInferenceChange(input: {
return {
...decideRectification({
methodCoverageAll: blockingMethodsCovered(collecting.methods),
- trainingGateOpen: training.length >= MIN_ACCEPTANCE_EVENTS
- && trainingDomains.size >= MIN_ACCEPTANCE_DOMAINS,
+ trainingGateOpen: dated.length >= MIN_ACCEPTANCE_EVENTS
+ && datedDomains.size >= MIN_ACCEPTANCE_DOMAINS,
candidateScores: input.state.candidates
.filter((item) => item.status !== "eliminated")
.map((item) => ({ time: item.time, score: item.posterior_score, window_index: item.window_index })),
diff --git a/frontend/src/lib/rectification-agentic/v9/evidence-model.ts b/frontend/src/lib/rectification-agentic/v9/evidence-model.ts
index 94c53201..442fa60d 100644
--- a/frontend/src/lib/rectification-agentic/v9/evidence-model.ts
+++ b/frontend/src/lib/rectification-agentic/v9/evidence-model.ts
@@ -4,6 +4,7 @@
*/
import { z } from "zod";
import { splitHoldoutEvents, type DatedEventInput } from "../core/split-holdout.ts";
+import { MIN_DATED_DOMAINS, MIN_DATED_EVENTS } from "../core/types.ts";
export const EVIDENCE_KINDS = [
"education_start",
@@ -230,9 +231,12 @@ export const NON_PRIMARY_SCORING_DOMAINS: ReadonlySet = new Set([
"other",
]);
-/** Same floors as `scripts/rectification/decision_policy.py`. Counted on training events only. */
-export const MIN_ACCEPTANCE_EVENTS = 3;
-export const MIN_ACCEPTANCE_DOMAINS = 2;
+/**
+ * Same floors as `scripts/rectification/decision_policy.py` (R1: 4 / 3).
+ * Counted on all dated primary-scoreable events: training plus the reserved holdout.
+ */
+export const MIN_ACCEPTANCE_EVENTS = MIN_DATED_EVENTS;
+export const MIN_ACCEPTANCE_DOMAINS = MIN_DATED_DOMAINS;
export function isBackgroundEvidenceKind(kind: EvidenceKind): boolean {
return BACKGROUND_ONLY_KINDS.has(kind);
@@ -280,9 +284,9 @@ function yearFromIso(value: string | null): number | null {
}
/**
- * Discrimination requires 3 training events / 2 training domains.
- * Holdout is reserved from the 4th dated event and does not count.
- * 3 collected events all stay in training.
+ * Discrimination and range delivery require 4 dated events across 3 domains
+ * (R1). The holdout is still reserved from the 4th dated event; it counts
+ * toward the floor but never toward scoring.
*/
export function trainingScoreableGate(evidence: readonly ScoreableEvidence[]): Readonly<{
trainingCount: number;
@@ -300,14 +304,15 @@ export function trainingScoreableGate(evidence: readonly ScoreableEvidence[]): R
}));
const split = splitHoldoutEvents(dated);
const training = split.filter((item) => item.usage === "training");
- const domains = new Set(training.map((item) => item.domain));
+ const trainingDomains = new Set(training.map((item) => item.domain));
const holdoutCount = split.filter((item) => item.usage === "holdout").length;
+ const datedDomains = new Set(scoreable.map((item) => item.domain));
return {
trainingCount: training.length,
- trainingDomainCount: domains.size,
+ trainingDomainCount: trainingDomains.size,
holdoutCount,
holdoutStatus: holdoutCount > 0 ? "reserved" : "not_reserved_min_events",
- open: training.length >= MIN_ACCEPTANCE_EVENTS && domains.size >= MIN_ACCEPTANCE_DOMAINS,
+ open: scoreable.length >= MIN_ACCEPTANCE_EVENTS && datedDomains.size >= MIN_ACCEPTANCE_DOMAINS,
};
}
diff --git a/frontend/tests/rectification-adopt-flow-fix-20260903.test.ts b/frontend/tests/rectification-adopt-flow-fix-20260903.test.ts
index 845b6446..e64fb8c5 100644
--- a/frontend/tests/rectification-adopt-flow-fix-20260903.test.ts
+++ b/frontend/tests/rectification-adopt-flow-fix-20260903.test.ts
@@ -71,6 +71,16 @@ const COLLECT_EVIDENCE = [
occurredTo: null,
eventKind: "relationship_start",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ id: "e-relocation",
+ status: "confirmed",
+ domain: "relocation",
+ datePrecision: "month",
+ occurredFrom: "2018-03-01",
+ occurredTo: null,
+ eventKind: "relocation",
+ },
] as const;
const OPENING_SKIPPED = {
@@ -96,6 +106,8 @@ function collectDecisionDossier(declinedTopics: ReadonlyArray = {}) {
@@ -195,7 +210,8 @@ test("unknown source opens a full-day block_scan window", () => {
);
});
-test("three dated events in two domains ask a block choice and never adopt", () => {
+// 原值: 题名「three dated events in two domains」/ 新值: 「four dated events in three domains」/ 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),块扫描门同口径
+test("four dated events in three domains ask a block choice and never adopt", () => {
const parsed = parseV9CaseDossier(blockDossier());
assert.ok(parsed);
const decision = decideFromDossier(parsed);
diff --git a/frontend/tests/rectification-collect-stall.test.ts b/frontend/tests/rectification-collect-stall.test.ts
index 9896cb12..88c64bdb 100644
--- a/frontend/tests/rectification-collect-stall.test.ts
+++ b/frontend/tests/rectification-collect-stall.test.ts
@@ -63,7 +63,7 @@ const CANDIDATES = [
{ id: "05:15", time: "05:15", relative_support: 6 },
] as const;
-const EVIDENCE = [
+const BASE_EVIDENCE = [
{
id: "e-career-entry",
status: "confirmed",
@@ -102,8 +102,22 @@ const EVIDENCE = [
},
] as const;
+// R1(BUG-1193):补第 3 域,保持原场景(原 4 件 2 域在旧门槛下训练门已开)。
+const EVIDENCE = [
+ ...BASE_EVIDENCE,
+ {
+ id: "e-finance",
+ status: "confirmed",
+ domain: "finance",
+ datePrecision: "month",
+ occurredFrom: "2022-03-01",
+ occurredTo: null,
+ eventKind: "finance_gain",
+ },
+] as const;
+
const LIVE_CASE_EVIDENCE = [
- ...EVIDENCE,
+ ...BASE_EVIDENCE,
{
id: "e-education",
status: "confirmed",
@@ -357,6 +371,8 @@ function revision5State(extraProbes: readonly ConflictProbe[] = []) {
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" },
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" },
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" },
+ // R1(BUG-1193):补第 3 域,保持原场景。
+ { id: "e-finance", domain: "finance", year: 2022, precision: "month" },
],
probes,
});
@@ -477,7 +493,9 @@ test("revision 5 with uncovered relatives asks the dated family collect, not a y
const state = revision5State();
assert.equal(state.revision, 5);
assert.equal(state.answered_probes.length, 4);
- assert.equal(state.events.find((item) => item.id === "e-rel-start")?.usage, "holdout");
+ // 原值: e-rel-start 为 holdout / 新值: 补的单件领域 e-finance 为 holdout(按领域分层留出单件域)/ 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),夹具补第 3 域
+ assert.equal(state.events.find((item) => item.id === "e-finance")?.usage, "holdout");
+ assert.equal(state.events.find((item) => item.id === "e-rel-start")?.usage, "training");
const unanswered = state.probes.filter((probe) => (
!state.answered_probes.some((item) => item.probe_id === probe.id)
));
@@ -1290,6 +1308,16 @@ test("yearless varga in another domain stays behind uncovered-method spoken coll
occurredTo: null,
eventKind: "education_start",
},
+ // R1(BUG-1193):补第 4 件,保持原场景(训练门已开)。
+ {
+ id: "e-career-2",
+ status: "confirmed",
+ domain: "career",
+ datePrecision: "year" as const,
+ occurredFrom: "2022-01-01",
+ occurredTo: null,
+ eventKind: "career_change",
+ },
{
id: "e-job",
status: "confirmed",
diff --git a/frontend/tests/rectification-collection-question-pool.test.ts b/frontend/tests/rectification-collection-question-pool.test.ts
index 2913623d..080a24fc 100644
--- a/frontend/tests/rectification-collection-question-pool.test.ts
+++ b/frontend/tests/rectification-collection-question-pool.test.ts
@@ -163,7 +163,17 @@ test("targeted collect names remaining split layers without inferred years", ()
// 原值: evidence = [educationStart, educationEnd, career](3 件 2 类,训练门已开)
// 新值: 毕业那件改成日期不明(不进训练门),门未开、覆盖的领域不变;门开的原夹具另断言池为空
// 原因: 门开后定向池恒空(BUG-1084,2026-09-29 D1);池本身的规则仍在门前生效,用门前夹具继续锁住
- assert.deepEqual(targetedCollectPool(layers, [educationStart, educationEnd, career], [], ["04:48", "05:07"], 4), []);
+ // R1(BUG-1193):门开夹具补第 4 件 / 第 3 域(搬家),保持「门开后池为空」原场景。
+ const relocation = {
+ status: "confirmed",
+ domain: "relocation",
+ datePrecision: "month",
+ occurredFrom: "2022-07-01",
+ occurredTo: null,
+ eventKind: "relocation",
+ summary: "搬到外地",
+ } as const;
+ assert.deepEqual(targetedCollectPool(layers, [educationStart, educationEnd, career, relocation], [], ["04:48", "05:07"], 4), []);
const evidence = [educationStart, { ...educationEnd, datePrecision: "unknown", occurredFrom: null, occurredTo: null }, career];
const splitTimes = ["04:48", "05:07"] as const;
const pool = targetedCollectPool(layers, evidence, [], splitTimes, 4);
diff --git a/frontend/tests/rectification-convergence-budget.test.ts b/frontend/tests/rectification-convergence-budget.test.ts
index 975e6b05..ab6e69a7 100644
--- a/frontend/tests/rectification-convergence-budget.test.ts
+++ b/frontend/tests/rectification-convergence-budget.test.ts
@@ -134,6 +134,8 @@ function inferenceState(
{ id: "e1", domain: "career", year: 2011, precision: "year", usage: "training" },
{ id: "e2", domain: "relationship", year: 2012, precision: "year", usage: "training" },
{ id: "e3", domain: "family", year: 2013, precision: "year", usage: "training" },
+ // R1(BUG-1193):补第 4 件(holdout),与 dossier 的 4 条证据一致,保持原场景。
+ { id: "e4", domain: "education", year: 2014, precision: "year", usage: "holdout" },
],
probes: [],
answered_probes: answers,
@@ -279,8 +281,11 @@ test("additional score evidence never widens the credible range", () => {
test("insufficient standalone evidence keeps collecting", () => {
for (const item of [
- { reason: "insufficient_dated_events" as const, input: { datedEventCount: 2, datedDomainCount: 2 } },
- { reason: "insufficient_domains" as const, input: { datedEventCount: 3, datedDomainCount: 1 } },
+ // 原值: {2 件/2 域}→insufficient_dated_events,{3 件/1 域}→insufficient_domains
+ // 新值: {3 件/3 域}→insufficient_dated_events,{4 件/2 域}→insufficient_domains
+ // 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),两个分支各取门槛下方一格
+ { reason: "insufficient_dated_events" as const, input: { datedEventCount: 3, datedDomainCount: 3 } },
+ { reason: "insufficient_domains" as const, input: { datedEventCount: 4, datedDomainCount: 2 } },
]) {
const decision = decideRectification({ ...BASE_INPUT, ...item.input });
assert.equal(decision.nextAction, "ask_fact_collection");
@@ -298,8 +303,8 @@ test("exhausted evidence-state stops complete with a usable range and the fixed
{
reason: "tied_first",
input: {
- datedEventCount: 3,
- datedDomainCount: 2,
+ datedEventCount: 4, // R1(BUG-1193):门槛上 3/2 → 4/3,保持原场景。
+ datedDomainCount: 3,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 34 },
@@ -310,7 +315,7 @@ test("exhausted evidence-state stops complete with a usable range and the fixed
},
{
reason: "user_uncertainty_too_high",
- input: { datedEventCount: 3, datedDomainCount: 2, userUncertaintyHigh: true },
+ input: { datedEventCount: 4, datedDomainCount: 3, userUncertaintyHigh: true }, // R1(BUG-1193):3/2 → 4/3
},
];
@@ -355,8 +360,8 @@ test("stale snapshots cannot complete or adopt an evidence-stop range", () => {
test("an exact first-place tie is distinct from a narrow 34/33/33 lead", () => {
const narrowLead = decideRectification({
...BASE_INPUT,
- datedEventCount: 3,
- datedDomainCount: 2,
+ datedEventCount: 4, // R1(BUG-1193):门槛上 3/2 → 4/3,保持原场景。
+ datedDomainCount: 3,
});
assert.equal(narrowLead.separation.tiedForFirst, false);
assert.equal(narrowLead.stopReason, undefined);
@@ -364,8 +369,8 @@ test("an exact first-place tie is distinct from a narrow 34/33/33 lead", () => {
const exactTie = decideRectification({
...BASE_INPUT,
- datedEventCount: 3,
- datedDomainCount: 2,
+ datedEventCount: 4, // R1(BUG-1193):门槛上 3/2 → 4/3,保持原场景。
+ datedDomainCount: 3,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 34 },
@@ -376,9 +381,14 @@ test("an exact first-place tie is distinct from a narrow 34/33/33 lead", () => {
assert.equal(exactTie.stopReason, "tied_first");
});
-test("standalone delivery floor remains separate from exact-minute confirmation", () => {
- assert.equal(MIN_STANDALONE_DATED_EVENTS, 3);
- assert.equal(MIN_STANDALONE_DATED_DOMAINS, 2);
+test("standalone delivery floor shares the 4/3 policy with exact-minute confirmation", () => {
+ // 原值: MIN_STANDALONE_DATED_EVENTS=3、MIN_STANDALONE_DATED_DOMAINS=2(区间门槛低于精确分钟确认门槛)
+ // 新值: 4、3,且与 RECTIFICATION_POLICY 同源
+ // 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),只保留 rectification_policy.v1.json 一处定义
+ assert.equal(MIN_STANDALONE_DATED_EVENTS, 4);
+ assert.equal(MIN_STANDALONE_DATED_DOMAINS, 3);
+ assert.equal(MIN_STANDALONE_DATED_EVENTS, RECTIFICATION_POLICY.minConfirmationEvents);
+ assert.equal(MIN_STANDALONE_DATED_DOMAINS, RECTIFICATION_POLICY.minConfirmationDomains);
assert.equal(RECTIFICATION_POLICY.minConfirmationEvents, 4);
assert.equal(RECTIFICATION_POLICY.minConfirmationDomains, 3);
});
@@ -393,10 +403,14 @@ test("dossier wiring counts only confirmed dated primary events", () => {
]));
assert.equal(decision.stopReason, "insufficient_dated_events");
+ // 原值: 3 条 career(3 件 1 域)→ insufficient_domains
+ // 新值: 4 条只覆盖 career + relationship(4 件 2 域)→ insufficient_domains
+ // 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),3 件会先落到 insufficient_dated_events
const oneDomain = decideFromDossier(dossier([
evidence("career", 1),
evidence("career", 2),
evidence("career", 3),
+ evidence("relationship", 4),
]));
assert.equal(oneDomain.stopReason, "insufficient_domains");
});
@@ -587,8 +601,8 @@ test("rectification label ladder follows evidence state and explicit adapter sup
const continuing = decideRectification({
...BASE_INPUT,
- datedEventCount: 3,
- datedDomainCount: 2,
+ datedEventCount: 4, // R1(BUG-1193):门槛上 3/2 → 4/3,保持原场景。
+ datedDomainCount: 3,
});
assert.equal(rectificationLabel({ decision: continuing }), "manual_pattern_consensus");
assert.equal(rectificationLabel({
@@ -602,8 +616,8 @@ test("rectification label ladder follows evidence state and explicit adapter sup
const tied = decideRectification({
...BASE_INPUT,
- datedEventCount: 3,
- datedDomainCount: 2,
+ datedEventCount: 4, // R1(BUG-1193):门槛上 3/2 → 4/3,保持原场景。
+ datedDomainCount: 3,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 34 },
diff --git a/frontend/tests/rectification-dead-d9-choice-20260916.test.ts b/frontend/tests/rectification-dead-d9-choice-20260916.test.ts
index a6c80c0c..eec795cf 100644
--- a/frontend/tests/rectification-dead-d9-choice-20260916.test.ts
+++ b/frontend/tests/rectification-dead-d9-choice-20260916.test.ts
@@ -57,6 +57,14 @@ const DATED = [
occurredFrom: "2016-01-01",
occurredTo: "2016-12-31",
},
+ // R1(BUG-1193):补第 4 件,保持原场景(训练门已开)。
+ {
+ status: "confirmed",
+ domain: "career",
+ datePrecision: "year",
+ occurredFrom: "2021-01-01",
+ occurredTo: "2021-12-31",
+ },
{
status: "confirmed",
domain: "occupation",
diff --git a/frontend/tests/rectification-decide-next-action.test.ts b/frontend/tests/rectification-decide-next-action.test.ts
index dc0425e0..19f8da40 100644
--- a/frontend/tests/rectification-decide-next-action.test.ts
+++ b/frontend/tests/rectification-decide-next-action.test.ts
@@ -325,7 +325,8 @@ test("user stop skips remaining collection and holdout when candidates already e
assert.equal(stopped.validated, false);
});
-test("discrimination waits for three training events after reserving holdout", () => {
+// 原值: 题名「waits for three training events」/ 新值: 「waits for four dated events across three domains」/ 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
+test("discrimination waits for four dated events across three domains after reserving holdout", () => {
const two = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
@@ -347,12 +348,22 @@ test("discrimination waits for three training events after reserving holdout", (
// 原值: 3 件即预留 holdout,trainingCount=2,门关,等到第 4 件才区分
// 新值: 恰好 3 件全部进训练,门开,holdout 仅 ≥4 才预留
// 原因: BUG-647 训练门与 holdout 解耦
- assert.equal(three.open, true);
+ // 原值: three.open=true / 新值: false / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193);3 件仍全部进训练、不留 holdout
+ assert.equal(three.open, false);
assert.equal(three.holdoutCount, 0);
assert.equal(three.trainingCount, 3);
+ // 4 件 3 域:留出 1 件 holdout 后训练 3 件,门槛按训练 + holdout 计,门开
assert.equal(four.open, true);
assert.equal(four.trainingCount, 3);
assert.equal(four.holdoutCount, 1);
+ // R1(BUG-1193):4 件但只有 2 域,门仍关
+ const fourTwoDomains = trainingScoreableGate([
+ { id: "e1", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
+ { id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
+ { id: "e3", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2022-10-01", occurredTo: null },
+ { id: "e4", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
+ ]);
+ assert.equal(fourTwoDomains.open, false);
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: two.open,
@@ -360,13 +371,14 @@ test("discrimination waits for three training events after reserving holdout", (
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_fact_collection");
+ // 原值: ask_candidate_discriminator / 新值: ask_fact_collection / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),3 件门未开
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: three.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
- }).type, "ask_candidate_discriminator");
+ }).type, "ask_fact_collection");
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: four.open,
diff --git a/frontend/tests/rectification-decision-authority.test.ts b/frontend/tests/rectification-decision-authority.test.ts
index efd5e917..cf98beda 100644
--- a/frontend/tests/rectification-decision-authority.test.ts
+++ b/frontend/tests/rectification-decision-authority.test.ts
@@ -86,8 +86,9 @@ function decideWithEngineCeiling(
candidateScores: SEPARATED,
holdoutValidation: "passed",
confirmationAllowed: true,
- datedEventCount: 3,
- datedDomainCount: 2,
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ datedEventCount: 4,
+ datedDomainCount: 3,
...overrides,
engineCeiling,
} as Parameters[0]);
@@ -104,9 +105,10 @@ test("invariant 1: TypeScript delivery capabilities never exceed the engine ceil
});
test("invariant 2: insufficient dated evidence never permits selection or adoption", () => {
- for (const datedEventCount of [0, 2, 3]) {
- for (const datedDomainCount of [0, 1, 2]) {
- if (datedEventCount >= 3 && datedDomainCount >= 2) continue;
+ // 原值: 事件 [0,2,3] × 领域 [0,1,2],跳过 ≥3 件且 ≥2 域 / 新值: 事件 [0,2,3,4] × 领域 [0,1,2,3],跳过 ≥4 件且 ≥3 域(3 件 2 域现在也必须挡住) / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
+ for (const datedEventCount of [0, 2, 3, 4]) {
+ for (const datedDomainCount of [0, 1, 2, 3]) {
+ if (datedEventCount >= 4 && datedDomainCount >= 3) continue;
const decision = decideWithEngineCeiling(ENGINE_OPEN, {
confirmationAllowed: false,
datedEventCount,
diff --git a/frontend/tests/rectification-delivery-vs-collect-20260914.test.ts b/frontend/tests/rectification-delivery-vs-collect-20260914.test.ts
index 2362bcd0..c33e864b 100644
--- a/frontend/tests/rectification-delivery-vs-collect-20260914.test.ts
+++ b/frontend/tests/rectification-delivery-vs-collect-20260914.test.ts
@@ -221,7 +221,8 @@ test("tied first place delivers a range even when narrowing flags are still open
candidateScores: TIED_CANDIDATES.map((item) => ({ time: item.time, score: item.relativeSupport })),
discriminatorProbe: null,
holdoutValidation: "unavailable",
- datedEventCount: 3,
+ // R1(BUG-1193):补第 4 件,保持原场景。
+ datedEventCount: 4,
datedDomainCount: 3,
refreshExhausted: false,
targetedCollectExhausted: true,
diff --git a/frontend/tests/rectification-discriminator-followup-consistency.test.ts b/frontend/tests/rectification-discriminator-followup-consistency.test.ts
index df1ba5c3..63d6b629 100644
--- a/frontend/tests/rectification-discriminator-followup-consistency.test.ts
+++ b/frontend/tests/rectification-discriminator-followup-consistency.test.ts
@@ -184,6 +184,16 @@ const EVIDENCE = [
occurredTo: null,
eventKind: "relationship_end",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ id: "e-move-2016",
+ status: "confirmed",
+ domain: "relocation",
+ datePrecision: "month",
+ occurredFrom: "2016-09-01",
+ occurredTo: null,
+ eventKind: "relocation",
+ },
] as const;
function contrastFromConflict(probe: ConflictProbe): CandidateDiscriminatorProbe {
@@ -219,6 +229,8 @@ function deadlockDossier(): { dossier: DecisionDossier; state: ReturnType dossierFixture({
- evidence: [educationEvidence, familyEvidence, careerEvidence],
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ evidence: [educationEvidence, familyEvidence, careerEvidence, careerChangeEvidence],
conversationSummary: {
confirmed_evidence_summary: [],
pending_revisions: [],
@@ -1565,8 +1577,9 @@ test("public tool surface stays at 14 and new cases bind 10.0.26", () => {
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
- datedEventCount: 3,
- datedDomainCount: 2,
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ datedEventCount: 4,
+ datedDomainCount: 3,
candidateScores: exhaustedCandidates.map((item) => ({
time: item.time,
score: item.relativeSupport,
@@ -1775,7 +1788,11 @@ test("precision stage d4 asks home change not family, and d5 asks education", ()
});
assert.notEqual(legacyTheme.next_followup?.intent, "distinguish_candidates");
const d5 = buildMethodFollowupPlan({
- evidence: covered.filter((item) => item.domain !== "education"),
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景(去掉学业后仍过门槛)。
+ evidence: [
+ ...covered.filter((item) => item.domain !== "education"),
+ { status: "confirmed" as const, domain: "relocation", datePrecision: "year" as const, occurredFrom: "2015-01-01", occurredTo: null },
+ ],
precisionStage: "d5_refine",
});
// 原值: 训练门开后不再为未覆盖学业轮转采集
@@ -1964,6 +1981,8 @@ test("d9_refine after relationship still asks uncovered career first", () => {
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2020-06-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null },
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景(事业仍未覆盖)。
+ { status: "confirmed", domain: "relocation", datePrecision: "month", occurredFrom: "2012-08-01", occurredTo: null },
],
precisionStage: "d9_refine",
});
@@ -2852,6 +2871,8 @@ test("three dated events with one holdout keep collecting instead of discriminat
datedEvidence("education", "2016"),
datedEvidence("career", "2020"),
datedEvidence("relationship", "2024"),
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景(训练仍是 3 件,门开)。
+ datedEvidence("relocation", "2012"),
{
status: "confirmed" as const,
domain: "occupation",
@@ -2867,7 +2888,8 @@ test("three dated events with one holdout keep collecting instead of discriminat
// 原因: BUG-647
assert.equal(gate.open, true);
assert.equal(gate.trainingCount, 3);
- assert.equal(gate.holdoutCount, 0);
+ // 原值: holdoutCount=0(3 件全训练) / 新值: 1(补的第 4 件起留 1 件 holdout,训练仍 3 件,BUG-647 的「holdout 不饿死训练」不变) / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
+ assert.equal(gate.holdoutCount, 1);
const plan = buildMethodFollowupPlan({
evidence,
declinedTopics: [{ target_domain: "family", status: "declined" }],
@@ -3141,12 +3163,13 @@ test("offer-candidates may proceed once method coverage leftover collect is clos
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
- evidence: [educationEvidence, relationshipEvidence, familyEvidence],
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ evidence: [educationEvidence, relationshipEvidence, familyEvidence, careerEvidence],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: scoreableFingerprintForRawEvidence(
- [educationEvidence, relationshipEvidence, familyEvidence],
+ [educationEvidence, relationshipEvidence, familyEvidence, careerEvidence],
),
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
diff --git a/frontend/tests/rectification-fewer-probes-card-20260926.test.ts b/frontend/tests/rectification-fewer-probes-card-20260926.test.ts
index ef78dbbe..d952eb84 100644
--- a/frontend/tests/rectification-fewer-probes-card-20260926.test.ts
+++ b/frontend/tests/rectification-fewer-probes-card-20260926.test.ts
@@ -259,7 +259,8 @@ test("D2 the skip re-ask and a pending year answer still hold the card", () => {
const yesOnWindow = [askedWindow(2019, 4, 6, "resolved")];
assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN, yesOnWindow, ["05:00", "05:15"], 3), false);
// Targeted lines still open: keep asking.
- assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN.slice(0, 3), [], ["05:00", "05:15"], 3), true);
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景(门开、定向线未问完)。slice(0, 3) → slice(0, 4)。
+ assert.equal(cardHoldingLinesExhausted(LAYERS, ALL_SEVEN.slice(0, 4), [], ["05:00", "05:15"], 3), true);
// Before the training gate the re-ask still holds the card.
assert.equal(cardHoldingLinesExhausted(LAYERS, [], skipped, ["05:00", "05:15"], 3), false);
});
diff --git a/frontend/tests/rectification-futile-collect-stop-20260929.test.ts b/frontend/tests/rectification-futile-collect-stop-20260929.test.ts
index 35e0973d..1d9f8c86 100644
--- a/frontend/tests/rectification-futile-collect-stop-20260929.test.ts
+++ b/frontend/tests/rectification-futile-collect-stop-20260929.test.ts
@@ -59,11 +59,13 @@ import {
const INVITE = /再补一件|补一件|再说一件|再对照几件经历|记得的人生经历|吻合率/;
-/** Three scoreable events in two domains open the training gate (holdout needs a fourth). */
+/** Four scoreable events in three domains open the training gate (R1, BUG-1193; the fourth is reserved as holdout). */
const GATE_OPEN: CollectionEvidence[] = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2012-06-01", occurredTo: null, eventKind: "education_completion" },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null, eventKind: "career_entry" },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2019-10-01", occurredTo: null, eventKind: "career_change" },
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ { status: "confirmed", domain: "relocation", datePrecision: "month", occurredFrom: "2014-08-01", occurredTo: null, eventKind: "relocation" },
];
function skippedOnce(domain: CollectKind) {
diff --git a/frontend/tests/rectification-occupation-dated-answer-20260911.test.ts b/frontend/tests/rectification-occupation-dated-answer-20260911.test.ts
index 0de0f5d7..b5c61614 100644
--- a/frontend/tests/rectification-occupation-dated-answer-20260911.test.ts
+++ b/frontend/tests/rectification-occupation-dated-answer-20260911.test.ts
@@ -228,9 +228,18 @@ test("dated occupation collect raises the training gate by one career event", ()
occurredFrom: "2020-06-01",
occurredTo: "2020-06-30",
eventKind: "education_completion",
+ }, {
+ // R1(BUG-1193):补第 3 件 / 第 2 域,让职业那一件仍是开门的最后一件(4 件 3 域)。
+ status: "confirmed" as const,
+ domain: "relocation",
+ datePrecision: "month" as const,
+ occurredFrom: "2018-07-01",
+ occurredTo: "2018-07-31",
+ eventKind: "relocation",
}];
const before = trainingScoreableGate(education);
- assert.equal(before.trainingCount, 2);
+ // 原值: 2 / 新值: 3(补了一件搬家) / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193)
+ assert.equal(before.trainingCount, 3);
assert.equal(before.open, false);
const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [datedOccupationInput()]);
const after = trainingScoreableGate([
@@ -244,7 +253,11 @@ test("dated occupation collect raises the training gate by one career event", ()
eventKind: item.eventKind,
})),
]);
- assert.equal(after.trainingCount, before.trainingCount + 1);
+ // 原值: after.trainingCount === before.trainingCount + 1(3 件全训练)
+ // 新值: 训练 + holdout === before + 1,holdoutCount === 1(第 4 件起留 1 件 holdout,训练仍 3 件)
+ // 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193);门槛按训练 + holdout 计,职业那一件仍是开门的一件
+ assert.equal(after.trainingCount + after.holdoutCount, before.trainingCount + 1);
+ assert.equal(after.holdoutCount, 1);
assert.equal(after.trainingDomainCount, 2);
assert.equal(after.open, true);
});
diff --git a/frontend/tests/rectification-open-collect-invite-20260914.test.ts b/frontend/tests/rectification-open-collect-invite-20260914.test.ts
index 9e630bb4..f4e8456e 100644
--- a/frontend/tests/rectification-open-collect-invite-20260914.test.ts
+++ b/frontend/tests/rectification-open-collect-invite-20260914.test.ts
@@ -62,6 +62,17 @@ const DATED_EVIDENCE = [
eventKind: "relocation",
summary: "搬家",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ id: "evidence-career-change",
+ status: "confirmed",
+ domain: "career",
+ datePrecision: "month",
+ occurredFrom: "2022-03-01",
+ occurredTo: null,
+ eventKind: "career_change",
+ summary: "换工作",
+ },
] as const;
function declineAllTargeted() {
@@ -110,6 +121,8 @@ function inference(overrides: Partial = {}): InferenceState {
{ id: "e1", domain: "career", year: 2020, precision: "month", usage: "training" },
{ id: "e2", domain: "education", year: 2016, precision: "month", usage: "training" },
{ id: "e3", domain: "relocation", year: 2018, precision: "month", usage: "training" },
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ { id: "e4", domain: "career", year: 2022, precision: "month", usage: "holdout" },
],
probes: [],
answered_probes: [],
diff --git a/frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts b/frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts
index 157c9aeb..0a5a0897 100644
--- a/frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts
+++ b/frontend/tests/rectification-probe-pool-exhausted-20260911.test.ts
@@ -109,7 +109,18 @@ const careerLeave = {
eventKind: "career_exit",
summary: "2020年10月离职",
};
-const EVIDENCE = [educationStart, educationEnd, careerIntern, careerLeave];
+// R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+const relocationMove = {
+ id: "e-relocation",
+ status: "confirmed" as const,
+ domain: "relocation",
+ datePrecision: "month" as const,
+ occurredFrom: "2018-07-01",
+ occurredTo: null,
+ eventKind: "home_change",
+ summary: "2018年7月搬家",
+};
+const EVIDENCE = [educationStart, educationEnd, careerIntern, careerLeave, relocationMove];
function uuidAt(index: number) {
return `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`;
@@ -282,6 +293,8 @@ function liveState(answeredCount: number) {
{ id: educationEnd.id, domain: "education", year: 2020, precision: "month" as const, usage: "training" as const },
{ id: careerIntern.id, domain: "career", year: 2020, precision: "month" as const, usage: "training" as const },
{ id: careerLeave.id, domain: "career", year: 2020, precision: "month" as const, usage: "holdout" as const },
+ // R1(BUG-1193):补第 3 域,保持原场景。
+ { id: relocationMove.id, domain: "relocation", year: 2018, precision: "month" as const, usage: "training" as const },
],
probes,
answered_probes: answered.map((probe) => ({
@@ -834,7 +847,8 @@ test("T4: exhausted refresh and declined targeted collect titles the card 目前
// 原因: BUG-654 卡片不得把当前范围写成结束
assert.equal(RECTIFICATION_USER_COPY.rangeDeliveryTitle, "目前范围");
const title = `${RECTIFICATION_USER_COPY.rangeDeliveryTitle} ${delivery.range?.[0]}–${delivery.range?.[1]}(对照了 ${delivery.event_count} 件经历)`;
- assert.match(title, /^目前范围 04:48–05:07(对照了 4 件经历)$/);
+ // 原值: 对照了 4 件经历 / 新值: 对照了 5 件经历 / 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),夹具补了第 3 域的一件搬家
+ assert.match(title, /^目前范围 04:48–05:07(对照了 5 件经历)$/);
assert.doesNotMatch(title, /这次给出|最终|结束| · /);
assert.match(delivery.narrow_hint ?? "", /现在还剩 04:48–05:07 里 6 个候选/);
// 原值: 收口改为不限领域的补充邀请
diff --git a/frontend/tests/rectification-range-offer-deadend.test.ts b/frontend/tests/rectification-range-offer-deadend.test.ts
index ac8c232d..c5fd08cf 100644
--- a/frontend/tests/rectification-range-offer-deadend.test.ts
+++ b/frontend/tests/rectification-range-offer-deadend.test.ts
@@ -115,6 +115,17 @@ const EVIDENCE = [
eventKind: "relationship_end",
summary: "2024-08-08 relationship_end",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ id: "e-edu-start",
+ status: "confirmed" as const,
+ domain: "education",
+ datePrecision: "month" as const,
+ occurredFrom: "2015-09-01",
+ occurredTo: null,
+ eventKind: "education_start",
+ summary: "2015-09-01 education_start",
+ },
] as const;
const DUAL_EXIT = "可以先按当前区间看盘,也可以再补一件记得时间的经历";
@@ -290,6 +301,8 @@ function liveState(remaining: readonly ConflictProbe[]): InferenceState {
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" as const, usage: "training" as const },
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" as const, usage: "training" as const },
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" as const, usage: "training" as const },
+ // R1(BUG-1193):补第 3 域,保持原场景。
+ { id: "e-edu-start", domain: "education", year: 2015, precision: "month" as const, usage: "training" as const },
],
probes,
answered_probes: answered,
diff --git a/frontend/tests/rectification-tie-break-entry-20260913.test.ts b/frontend/tests/rectification-tie-break-entry-20260913.test.ts
index ddf9ee87..f90781aa 100644
--- a/frontend/tests/rectification-tie-break-entry-20260913.test.ts
+++ b/frontend/tests/rectification-tie-break-entry-20260913.test.ts
@@ -44,6 +44,16 @@ const LEDGER = [
eventKind: "relationship_start",
summary: "确定关系",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ status: "confirmed",
+ domain: "relocation",
+ datePrecision: "month",
+ occurredFrom: "2018-07-01",
+ occurredTo: null,
+ eventKind: "relocation",
+ summary: "搬到外地",
+ },
] as const;
function contrastProbe(layer: "d9" | "d10") {
diff --git a/frontend/tests/rectification-tiebreak-before-card-20260914.test.ts b/frontend/tests/rectification-tiebreak-before-card-20260914.test.ts
index 2df80cfa..d6e20cc1 100644
--- a/frontend/tests/rectification-tiebreak-before-card-20260914.test.ts
+++ b/frontend/tests/rectification-tiebreak-before-card-20260914.test.ts
@@ -82,6 +82,16 @@ function planInput(askedProbeKeys: readonly string[] = []) {
eventKind: "relationship_start",
summary: "确定关系",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ status: "confirmed",
+ domain: "relocation",
+ datePrecision: "month",
+ occurredFrom: "2018-07-01",
+ occurredTo: null,
+ eventKind: "relocation",
+ summary: "搬到外地",
+ },
],
declinedTopics: [],
sessionOutcome: "discriminate_candidates" as const,
@@ -113,7 +123,8 @@ function tiedDecision(overrides: Partial[
],
discriminatorProbe: null,
holdoutValidation: "unavailable",
- datedEventCount: 3,
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ datedEventCount: 4,
datedDomainCount: 3,
targetedCollectExhausted: true,
refreshExhausted: true,
@@ -134,7 +145,8 @@ function convergingDecision(overrides: Partial {
occurredTo: null,
eventKind: "relocation",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ status: "confirmed",
+ domain: "education",
+ datePrecision: "month",
+ occurredFrom: "2020-06-01",
+ occurredTo: null,
+ eventKind: "education_milestone",
+ },
];
const declined = [
{ questionId: "collect:targeted:relationship", status: "declined", target_kind: "targeted:relationship" },
diff --git a/frontend/tests/rectification-tied-first-fix-20260914.test.ts b/frontend/tests/rectification-tied-first-fix-20260914.test.ts
index 4a46e3b0..af9a6948 100644
--- a/frontend/tests/rectification-tied-first-fix-20260914.test.ts
+++ b/frontend/tests/rectification-tied-first-fix-20260914.test.ts
@@ -62,8 +62,9 @@ function tiedInput(overrides: Partial[0]>
trainingGateOpen: true,
candidateScores: TIED_THREE.map((item) => ({ time: item.time, score: item.score })),
holdoutValidation: "unavailable" as const,
- datedEventCount: 3,
- datedDomainCount: 2,
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ datedEventCount: 4,
+ datedDomainCount: 3,
inferenceCredibleRange: OPENING,
openingCandidateRange: OPENING,
...overrides,
@@ -110,7 +111,8 @@ test("tied first place with no probe and targeted collect exhausted delivers a r
test("a live first-place tie with remaining probes does not announce adopt_representative", () => {
const decision = decideRectification(tiedInput({
discriminatorProbe: DATED_PROBE,
- datedEventCount: 3,
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ datedEventCount: 4,
targetedCollectExhausted: false,
refreshExhausted: false,
}));
diff --git a/frontend/tests/rectification-unstampable-probe-20260914.test.ts b/frontend/tests/rectification-unstampable-probe-20260914.test.ts
index 04eaecf6..5d26b36c 100644
--- a/frontend/tests/rectification-unstampable-probe-20260914.test.ts
+++ b/frontend/tests/rectification-unstampable-probe-20260914.test.ts
@@ -184,6 +184,17 @@ const DATED_EVIDENCE = [
eventKind: "relocation",
summary: "2015年搬家",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景(PERSIST_EVIDENCE 由 3 件变 4 件)。
+ {
+ id: "e-promo",
+ status: "confirmed" as const,
+ domain: "career",
+ datePrecision: "month" as const,
+ occurredFrom: "2022-03-01",
+ occurredTo: null,
+ eventKind: "promotion",
+ summary: "2022年3月升职",
+ },
];
const PERSIST_EVIDENCE = DATED_EVIDENCE.filter((row) => row.domain !== "relationship");
diff --git a/frontend/tests/rectification-v9-agent.test.ts b/frontend/tests/rectification-v9-agent.test.ts
index 617b4078..40043be1 100644
--- a/frontend/tests/rectification-v9-agent.test.ts
+++ b/frontend/tests/rectification-v9-agent.test.ts
@@ -1032,6 +1032,21 @@ test("delivery persist keeps three sentences when the model writes four", async
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:06.000Z",
},
+ // R1(BUG-1193):补第 4 件 / 第 3 域,保持原场景。
+ {
+ id: "e-promo",
+ source_turn_id: TURN_ID,
+ subject: "self",
+ event_kind: "promotion",
+ domain: "career",
+ occurred_from: "2019-03-01",
+ occurred_to: null,
+ date_precision: "month",
+ summary: "2019年3月升职",
+ status: "confirmed",
+ supersedes_evidence_id: null,
+ created_at: "2026-08-12T10:00:06.000Z",
+ },
],
}),
get_agentic_rectification_case_compute: () => computeFixture(),
diff --git a/scripts/rectification/decision_policy.py b/scripts/rectification/decision_policy.py
index 90cad86c..102ac00e 100644
--- a/scripts/rectification/decision_policy.py
+++ b/scripts/rectification/decision_policy.py
@@ -26,13 +26,16 @@ from scripts.rectification_policy import (
MIN_CONFIRMATION_MARGIN_PERCENT,
)
-POLICY_VERSION = "rectification-candidate-policy-v3"
+POLICY_VERSION = "rectification-candidate-policy-v4"
RECEIPT_VERSION = "candidate-decision-receipt-v2"
EXECUTION_LEDGER_VERSION = "rectification-execution-ledger-v2"
SCORE_QUANTUM = Decimal("0.0001")
TIE_ABSOLUTE_TOLERANCE = Decimal("0.0001")
-MIN_ACCEPTANCE_EVENTS = 3
-MIN_ACCEPTANCE_DOMAINS = 2
+# R1 (2026-10-02, BUG-1193): a range is accepted only with at least 4 dated
+# scoreable events across 3 domains, counted on all of them (training + the
+# reserved holdout). Single definition: references/rectification_policy.v1.json.
+MIN_ACCEPTANCE_EVENTS = MIN_CONFIRMATION_EVENTS
+MIN_ACCEPTANCE_DOMAINS = MIN_CONFIRMATION_DOMAINS
MIN_DATE_QUALITY_MEAN = Decimal("0.65")
MIN_DIAGNOSTIC_RETENTION = Decimal("0.75")
MIN_ACCEPTANCE_MARGIN_PERCENT = Decimal("10")
@@ -543,17 +546,19 @@ def build_decision_receipt(
if str(event.get("id") or "") not in holdout
]
domains = sorted({event["domain"] for event in training_events})
+ scoreable_domains = sorted({event["domain"] for event in scoreable_events})
candidate_presence = _gate(bool(candidate_decisions), candidate_count=len(candidate_decisions))
event_quality = _gate(
- len(training_events) >= MIN_ACCEPTANCE_EVENTS,
- scoreable_event_count=len(training_events),
+ len(scoreable_events) >= MIN_ACCEPTANCE_EVENTS,
+ scoreable_event_count=len(scoreable_events),
+ training_event_count=len(training_events),
minimum=MIN_ACCEPTANCE_EVENTS,
)
domain_diversity = _gate(
- len(domains) >= MIN_ACCEPTANCE_DOMAINS,
- scoreable_domain_count=len(domains),
+ len(scoreable_domains) >= MIN_ACCEPTANCE_DOMAINS,
+ scoreable_domain_count=len(scoreable_domains),
minimum=MIN_ACCEPTANCE_DOMAINS,
- domains=domains,
+ domains=scoreable_domains,
)
date_quality = _date_quality(training_events)
top_tied_count = candidate_decisions[0]["tied_minute_count"] if candidate_decisions else 0
diff --git a/tests/golden/rectification_engine_memoization_v3.json b/tests/golden/rectification_engine_memoization_v3.json
new file mode 100644
index 00000000..4ec5542c
--- /dev/null
+++ b/tests/golden/rectification_engine_memoization_v3.json
@@ -0,0 +1,1792 @@
+{
+ "source_commit": "7fb165ecf66ba1726f2fc74ffa2353fadc65ba39",
+ "candidate_scores": [
+ {
+ "time": "12:00",
+ "score": 8.4977,
+ "supporting_event_ids": [
+ "00000000-0000-4000-8000-000000000001",
+ "00000000-0000-4000-8000-000000000002"
+ ],
+ "conflicting_event_ids": []
+ },
+ {
+ "time": "12:01",
+ "score": 8.493,
+ "supporting_event_ids": [
+ "00000000-0000-4000-8000-000000000001",
+ "00000000-0000-4000-8000-000000000002"
+ ],
+ "conflicting_event_ids": []
+ },
+ {
+ "time": "12:02",
+ "score": 8.0521,
+ "supporting_event_ids": [
+ "00000000-0000-4000-8000-000000000001",
+ "00000000-0000-4000-8000-000000000002"
+ ],
+ "conflicting_event_ids": []
+ }
+ ],
+ "decision_receipt": {
+ "receipt_version": "candidate-decision-receipt-v2",
+ "contract_version": "v2",
+ "event_contract_version": "rectification-event-contract-v2",
+ "policy_version": "rectification-candidate-policy-v4",
+ "decision_policy_version": "rectification-candidate-policy-v4",
+ "display_allowed": true,
+ "selection_allowed": false,
+ "acceptance_allowed": false,
+ "propose_allowed": false,
+ "confirmation_allowed": false,
+ "accept_allowed": false,
+ "confirm_allowed": false,
+ "representative_candidate_id": "80d8dd80-09fb-57a0-86e3-fcda9e428add",
+ "representative_time": "12:00",
+ "overall_confidence": "low",
+ "margin_percent": 5.2438,
+ "reasons": [
+ "insufficient_events",
+ "insufficient_domain_diversity",
+ "insufficient_diagnostic_stability",
+ "insufficient_confirmation_events",
+ "insufficient_confirmation_domains",
+ "insufficient_confirmation_margin",
+ "engine_exact_confirmation_not_granted",
+ "external_validation_not_passed",
+ "public_aa_holdout_not_ready"
+ ],
+ "acceptance_reasons": [
+ "insufficient_events",
+ "insufficient_domain_diversity"
+ ],
+ "confirmation_reasons": [
+ "insufficient_diagnostic_stability",
+ "insufficient_confirmation_events",
+ "insufficient_confirmation_domains",
+ "insufficient_confirmation_margin",
+ "engine_exact_confirmation_not_granted",
+ "external_validation_not_passed",
+ "public_aa_holdout_not_ready"
+ ],
+ "tie_policy": {
+ "score_quantum": 0.0001,
+ "absolute_tolerance": 0.0001,
+ "rounding": "ROUND_HALF_UP"
+ },
+ "gates": {
+ "candidate_presence": {
+ "passed": true,
+ "candidate_count": 2
+ },
+ "event_quality": {
+ "passed": false,
+ "scoreable_event_count": 2,
+ "training_event_count": 2,
+ "minimum": 4
+ },
+ "domain_diversity": {
+ "passed": false,
+ "scoreable_domain_count": 2,
+ "minimum": 3,
+ "domains": [
+ "career",
+ "education"
+ ]
+ },
+ "date_quality": {
+ "passed": true,
+ "precision_weight_total": 1.3,
+ "precision_weight_mean": 0.65,
+ "minimum_precision_weight_mean": 0.65,
+ "low_reliability_event_ids": [],
+ "unresolved_conflict_event_ids": []
+ },
+ "unique_top": {
+ "passed": true,
+ "tied_minute_count": 1
+ },
+ "diagnostic_quality": {
+ "passed": false,
+ "minimum_retention": 0.75,
+ "minimum_margin_percent": 10.0,
+ "margin_percent": 5.2438,
+ "leave_one_event_out_retention_rate": 1.0,
+ "leave_one_domain_out_retention_rate": 1.0,
+ "date_sensitivity_retention_rate": 0.9583333333333333
+ },
+ "required_layers": {
+ "passed": true,
+ "missing_layers": [],
+ "skipped_by_policy": []
+ },
+ "exact_confirmation": {
+ "passed": false,
+ "fail_closed": true,
+ "engine_granted": false,
+ "adjacent_passed": true,
+ "indistinguishable_width_minutes": 3,
+ "max_confirmation_width_minutes": 5,
+ "external_validation_status": "not_evaluated",
+ "unique_minute_path": "closed_at_representative",
+ "required_scoreable_events": 4,
+ "required_scoreable_domains": 3,
+ "reason": "engine_and_external_validation_must_explicitly_pass",
+ "holdout": {
+ "sealed_benchmark_id": "minute_rectification_fact_ranker_v4_holdout_v3",
+ "status": "not_ready",
+ "valid_public_aa_cases": 20,
+ "required_cases": 20,
+ "top_1_rate": 0.15,
+ "confirmation_coverage_rate": 0.0
+ }
+ }
+ },
+ "house_table": {
+ "time": "12:00",
+ "lagna": "\u53cc\u9c7c\u5ea7",
+ "houses": [
+ {
+ "house": 1,
+ "sign": "\u53cc\u9c7c\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 2,
+ "sign": "\u767d\u7f8a\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 3,
+ "sign": "\u91d1\u725b\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 4,
+ "sign": "\u53cc\u5b50\u5ea7",
+ "occupants": [
+ "\u6728\u661f"
+ ]
+ },
+ {
+ "house": 5,
+ "sign": "\u5de8\u87f9\u5ea7",
+ "occupants": [
+ "\u8ba1\u90fd"
+ ]
+ },
+ {
+ "house": 6,
+ "sign": "\u72ee\u5b50\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 7,
+ "sign": "\u5904\u5973\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 8,
+ "sign": "\u5929\u79e4\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 9,
+ "sign": "\u5929\u874e\u5ea7",
+ "occupants": [
+ "\u706b\u661f"
+ ]
+ },
+ {
+ "house": 10,
+ "sign": "\u5c04\u624b\u5ea7",
+ "occupants": [
+ "\u592a\u9633",
+ "\u571f\u661f"
+ ]
+ },
+ {
+ "house": 11,
+ "sign": "\u6469\u7faf\u5ea7",
+ "occupants": [
+ "\u6c34\u661f",
+ "\u91d1\u661f",
+ "\u7f57\u777a"
+ ]
+ },
+ {
+ "house": 12,
+ "sign": "\u6c34\u74f6\u5ea7",
+ "occupants": [
+ "\u6708\u4eae"
+ ]
+ }
+ ]
+ },
+ "natal_recast": {
+ "time": "12:00",
+ "lagna": "\u53cc\u9c7c\u5ea7",
+ "user_meaning": "\u672c\u547d\u5bab\u4f4d\u5df2\u6309 12:00 \u91cd\u7b97\uff08\u4e0a\u5347 \u53cc\u9c7c\u5ea7\uff09\u3002\u4e0b\u9762\u662f\u672c\u8f6e\u5b9e\u9645\u6267\u884c\u7684\u6280\u6cd5\uff0c\u4e0d\u80fd\u5f53\u4f5c\u552f\u4e00\u5206\u949f\u786e\u8ba4\u3002",
+ "unique_minute_claim": false,
+ "confirmation_allowed": false
+ },
+ "house_tables_by_time": {
+ "12:00": {
+ "time": "12:00",
+ "lagna": "\u53cc\u9c7c\u5ea7",
+ "houses": [
+ {
+ "house": 1,
+ "sign": "\u53cc\u9c7c\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 2,
+ "sign": "\u767d\u7f8a\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 3,
+ "sign": "\u91d1\u725b\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 4,
+ "sign": "\u53cc\u5b50\u5ea7",
+ "occupants": [
+ "\u6728\u661f"
+ ]
+ },
+ {
+ "house": 5,
+ "sign": "\u5de8\u87f9\u5ea7",
+ "occupants": [
+ "\u8ba1\u90fd"
+ ]
+ },
+ {
+ "house": 6,
+ "sign": "\u72ee\u5b50\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 7,
+ "sign": "\u5904\u5973\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 8,
+ "sign": "\u5929\u79e4\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 9,
+ "sign": "\u5929\u874e\u5ea7",
+ "occupants": [
+ "\u706b\u661f"
+ ]
+ },
+ {
+ "house": 10,
+ "sign": "\u5c04\u624b\u5ea7",
+ "occupants": [
+ "\u592a\u9633",
+ "\u571f\u661f"
+ ]
+ },
+ {
+ "house": 11,
+ "sign": "\u6469\u7faf\u5ea7",
+ "occupants": [
+ "\u6c34\u661f",
+ "\u91d1\u661f",
+ "\u7f57\u777a"
+ ]
+ },
+ {
+ "house": 12,
+ "sign": "\u6c34\u74f6\u5ea7",
+ "occupants": [
+ "\u6708\u4eae"
+ ]
+ }
+ ]
+ },
+ "12:01": {
+ "time": "12:01",
+ "lagna": "\u53cc\u9c7c\u5ea7",
+ "houses": [
+ {
+ "house": 1,
+ "sign": "\u53cc\u9c7c\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 2,
+ "sign": "\u767d\u7f8a\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 3,
+ "sign": "\u91d1\u725b\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 4,
+ "sign": "\u53cc\u5b50\u5ea7",
+ "occupants": [
+ "\u6728\u661f"
+ ]
+ },
+ {
+ "house": 5,
+ "sign": "\u5de8\u87f9\u5ea7",
+ "occupants": [
+ "\u8ba1\u90fd"
+ ]
+ },
+ {
+ "house": 6,
+ "sign": "\u72ee\u5b50\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 7,
+ "sign": "\u5904\u5973\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 8,
+ "sign": "\u5929\u79e4\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 9,
+ "sign": "\u5929\u874e\u5ea7",
+ "occupants": [
+ "\u706b\u661f"
+ ]
+ },
+ {
+ "house": 10,
+ "sign": "\u5c04\u624b\u5ea7",
+ "occupants": [
+ "\u592a\u9633",
+ "\u571f\u661f"
+ ]
+ },
+ {
+ "house": 11,
+ "sign": "\u6469\u7faf\u5ea7",
+ "occupants": [
+ "\u6c34\u661f",
+ "\u91d1\u661f",
+ "\u7f57\u777a"
+ ]
+ },
+ {
+ "house": 12,
+ "sign": "\u6c34\u74f6\u5ea7",
+ "occupants": [
+ "\u6708\u4eae"
+ ]
+ }
+ ]
+ },
+ "12:02": {
+ "time": "12:02",
+ "lagna": "\u53cc\u9c7c\u5ea7",
+ "houses": [
+ {
+ "house": 1,
+ "sign": "\u53cc\u9c7c\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 2,
+ "sign": "\u767d\u7f8a\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 3,
+ "sign": "\u91d1\u725b\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 4,
+ "sign": "\u53cc\u5b50\u5ea7",
+ "occupants": [
+ "\u6728\u661f"
+ ]
+ },
+ {
+ "house": 5,
+ "sign": "\u5de8\u87f9\u5ea7",
+ "occupants": [
+ "\u8ba1\u90fd"
+ ]
+ },
+ {
+ "house": 6,
+ "sign": "\u72ee\u5b50\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 7,
+ "sign": "\u5904\u5973\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 8,
+ "sign": "\u5929\u79e4\u5ea7",
+ "occupants": []
+ },
+ {
+ "house": 9,
+ "sign": "\u5929\u874e\u5ea7",
+ "occupants": [
+ "\u706b\u661f"
+ ]
+ },
+ {
+ "house": 10,
+ "sign": "\u5c04\u624b\u5ea7",
+ "occupants": [
+ "\u592a\u9633",
+ "\u571f\u661f"
+ ]
+ },
+ {
+ "house": 11,
+ "sign": "\u6469\u7faf\u5ea7",
+ "occupants": [
+ "\u6c34\u661f",
+ "\u91d1\u661f",
+ "\u7f57\u777a"
+ ]
+ },
+ {
+ "house": 12,
+ "sign": "\u6c34\u74f6\u5ea7",
+ "occupants": [
+ "\u6708\u4eae"
+ ]
+ }
+ ]
+ }
+ },
+ "technique_audit_table": [
+ {
+ "technique": "D1 \u672c\u547d\u76d8",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u6309\u8be5\u5206\u949f\u91cd\u7b97\u672c\u547d\u5bab\u4f4d\u3002"
+ },
+ {
+ "technique": "D5 \u6210\u5c31\u5206\u76d8",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5b66\u4e1a\u6216\u88ab\u59d4\u4ee5\u8d23\u4efb\u7684\u53d8\u5316\u3002"
+ },
+ {
+ "technique": "D10 \u4e8b\u4e1a\u5206\u76d8",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u4e8b\u4e1a\u4e3b\u9898\uff0c\u53ef\u7528 D10 \u4e0a\u5347\u7c7b\u578b\u8868\u4f5c\u6821\u65f6\u65b9\u6cd5\uff0c\u4e0d\u662f\u547d\u8fd0\u627f\u8bfa\u3002"
+ },
+ {
+ "technique": "D24 \u6559\u80b2\u5206\u76d8",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5b66\u4e1a\u4e3b\u9898\u3002"
+ },
+ {
+ "technique": "Vimshottari",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u4e3b\u9650\u3002"
+ },
+ {
+ "technique": "Narayana",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u5206\u76d8\u5927\u8fd0\u3002"
+ },
+ {
+ "technique": "Ashtakavarga",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u505a Ashtakavarga \u8f85\u52a9\u5bf9\u7167\u3002"
+ },
+ {
+ "technique": "Shadbala",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u505a\u5df2\u6838\u9a8c\u7684 Shadbala \u5206\u91cf\u8f85\u52a9\u5bf9\u7167\u3002"
+ },
+ {
+ "technique": "\u6362\u8fd0\u8d34\u8fd1\u5ea6",
+ "status": "executed",
+ "note": "\u672c\u8f6e\u5df2\u5bf9\u7167\u65e5\u7ea7\u4e8b\u4ef6\u4e0e\u5019\u9009\u6362\u8fd0\u65e5\u671f\u7684\u8d34\u8fd1\u7a0b\u5ea6\u3002"
+ },
+ {
+ "technique": "KP \u5bab\u5934",
+ "status": "executed",
+ "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002"
+ },
+ {
+ "technique": "VedAstro \u5206\u949f\u7ea7\u6821\u9a8c",
+ "status": "blocked",
+ "note": "\u5b98\u65b9\u5206\u949f\u654f\u611f\u6821\u9a8c\u5c1a\u672a\u8dd1\u901a\u3002\u672a\u8c03\u7528\u4e0d\u7b49\u4e8e\u5931\u8d25\uff0c\u4f46\u7f3a\u8fd9\u4e00\u5c42\u4e0d\u80fd\u5199\u786e\u8ba4\u3002"
+ },
+ {
+ "technique": "\u552f\u4e00\u5206\u949f\u786e\u8ba4",
+ "status": "blocked",
+ "note": "\u672c\u4f1a\u8bdd\u4ee5\u4ee3\u8868\u6027\u65f6\u95f4\u6536\u53e3\uff0c\u4e0d\u786e\u8ba4\u552f\u4e00\u5206\u949f\u3002"
+ }
+ ],
+ "window_scan": {
+ "scanned": true,
+ "confirmation_allowed": false,
+ "unique_minute_claim": false,
+ "transitions": [
+ {
+ "layer": "kp1",
+ "at": "12:01",
+ "user_meaning": "KP 1\u5bab\u5b50\u4e3b \u5728 12:01 \u53d1\u751f\u53d8\u5316",
+ "from_sign": "\u5c04\u624b\u5ea7",
+ "to_sign": "\u767d\u7f8a\u5ea7"
+ },
+ {
+ "layer": "d9",
+ "at": "12:02",
+ "user_meaning": "D9 \u5728 12:02 \u53d1\u751f\u53d8\u5316",
+ "from_sign": "\u5c04\u624b\u5ea7",
+ "to_sign": "\u6469\u7faf\u5ea7"
+ },
+ {
+ "layer": "d12",
+ "at": "12:02",
+ "user_meaning": "D12 \u5728 12:02 \u53d1\u751f\u53d8\u5316",
+ "from_sign": "\u5929\u79e4\u5ea7",
+ "to_sign": "\u5929\u874e\u5ea7"
+ },
+ {
+ "layer": "d24",
+ "at": "12:02",
+ "user_meaning": "D24 \u5728 12:02 \u53d1\u751f\u53d8\u5316",
+ "from_sign": "\u5929\u79e4\u5ea7",
+ "to_sign": "\u5929\u874e\u5ea7"
+ },
+ {
+ "layer": "d30",
+ "at": "12:02",
+ "user_meaning": "D30 \u5728 12:02 \u53d1\u751f\u53d8\u5316",
+ "from_sign": "\u6469\u7faf\u5ea7",
+ "to_sign": "\u5929\u874e\u5ea7"
+ },
+ {
+ "layer": "pada",
+ "at": "12:02",
+ "user_meaning": "Nakshatra pada \u5728 12:02 \u53d1\u751f\u53d8\u5316"
+ }
+ ],
+ "d1_lagna_count": 1,
+ "d1_candidates_differ": false,
+ "d9_lagna_count": 2,
+ "d9_candidates_differ": true,
+ "d10_lagna_count": 1,
+ "d10_candidates_differ": false,
+ "d4_lagna_count": 1,
+ "d4_candidates_differ": false,
+ "d5_lagna_count": 1,
+ "d5_candidates_differ": false,
+ "d7_lagna_count": 1,
+ "d7_candidates_differ": false,
+ "d12_lagna_count": 2,
+ "d12_candidates_differ": true,
+ "d24_lagna_count": 2,
+ "d24_candidates_differ": true,
+ "d2_lagna_count": 1,
+ "d2_candidates_differ": false,
+ "d11_lagna_count": 1,
+ "d11_candidates_differ": false,
+ "d30_lagna_count": 2,
+ "d30_candidates_differ": true,
+ "pada_count": 2,
+ "pada_candidates_differ": true,
+ "hora_count": 1,
+ "hora_candidates_differ": false,
+ "ghati_count": 1,
+ "ghati_candidates_differ": false,
+ "bhava_count": 1,
+ "bhava_candidates_differ": false,
+ "pranapada_count": 1,
+ "pranapada_candidates_differ": false,
+ "kp1_count": 2,
+ "kp1_candidates_differ": true,
+ "kp4_count": 1,
+ "kp4_candidates_differ": false,
+ "kp7_count": 1,
+ "kp7_candidates_differ": false,
+ "kp10_count": 1,
+ "kp10_candidates_differ": false,
+ "d9_sign_names": [
+ "\u5c04\u624b\u5ea7",
+ "\u6469\u7faf\u5ea7"
+ ],
+ "d10_sign_names": [
+ "\u91d1\u725b\u5ea7"
+ ]
+ },
+ "event_dasha_ledger": [
+ {
+ "summary": "\u5165\u5b66",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "education",
+ "year_month": "2008-01"
+ },
+ {
+ "summary": "\u5165\u804c",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "career",
+ "year_month": "2012-06"
+ }
+ ],
+ "event_dasha_ledger_by_time": {
+ "12:00": [
+ {
+ "summary": "\u5165\u5b66",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "education",
+ "year_month": "2008-01"
+ },
+ {
+ "summary": "\u5165\u804c",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "career",
+ "year_month": "2012-06"
+ }
+ ],
+ "12:02": [
+ {
+ "summary": "\u5165\u5b66",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u5b66\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\u3001\u5206\u76d8\u5927\u8fd0\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "education",
+ "year_month": "2008-01"
+ },
+ {
+ "summary": "\u5165\u804c",
+ "match": "strong",
+ "match_label": "\u5f3a\u76f8\u5173",
+ "tracks": [
+ "vimshottari"
+ ],
+ "gochara": "not_seen",
+ "user_meaning": "\u5165\u804c\uff1a\u5f3a\u76f8\u5173\uff08\u4e3b\u9650\uff1bGochara \u672a\u89c1\u5bf9\u5e94\uff09",
+ "domain": "career",
+ "year_month": "2012-06"
+ }
+ ]
+ },
+ "prospective_windows_by_time": {
+ "12:00": [
+ {
+ "domain": "career",
+ "from": "2027-02",
+ "to": "2027-02"
+ },
+ {
+ "domain": "career",
+ "from": "2027-06",
+ "to": "2027-06"
+ }
+ ],
+ "12:02": [
+ {
+ "domain": "career",
+ "from": "2027-01",
+ "to": "2027-01"
+ },
+ {
+ "domain": "career",
+ "from": "2027-06",
+ "to": "2027-06"
+ }
+ ]
+ },
+ "event_fit_rate": {
+ "matched": 2,
+ "total": 2,
+ "percent": 100,
+ "band": "high",
+ "label": "\u9ad8\u5ea6\u543b\u5408\uff08\u4e8b\u4ef6\u543b\u5408\u7387 \u226580%\uff09",
+ "unique_minute_claim": false,
+ "user_meaning": "\u5f53\u524d\u7a97 2/2 \u4ef6\u5df2\u786e\u8ba4\u4e8b\u4ef6\u4e0e Dasha/Gochara \u543b\u5408\uff0c\u9ad8\u5ea6\u543b\u5408\uff08\u4e8b\u4ef6\u543b\u5408\u7387 \u226580%\uff09\u3002\u8fd9\u662f\u76f8\u5bf9\u62df\u5408\uff0c\u4e0d\u662f\u5df2\u786e\u8ba4\u552f\u4e00\u51fa\u751f\u5206\u949f\u3002"
+ },
+ "dasha_agreement": {
+ "status": "agree",
+ "vimshottari_top": "12:00",
+ "narayana_top": "12:00",
+ "user_meaning": "\u4e3b\u9650\u548c\u5206\u76d8\u5927\u8fd0\u90fd\u66f4\u652f\u6301\u540c\u4e00\u6bb5\u4ee3\u8868\u6027\u65f6\u95f4\u3002\u8fd9\u4ecd\u4e0d\u662f\u552f\u4e00\u5206\u949f\u786e\u8ba4\u3002"
+ },
+ "lagna_contrast": null,
+ "nakshatra_boundary": {
+ "near_boundary": false,
+ "distance_degrees": 2.886,
+ "user_meaning": null,
+ "options": []
+ },
+ "precision_stage": {
+ "current": "d9_refine",
+ "can_stop": true,
+ "user_meaning": "\u672c\u547d\u4e0a\u5347\u5df2\u8f83\u7a33\uff0c\u5173\u7cfb\u76d8\u4ecd\u4f1a\u6362\u5347\u3002\u53ef\u518d\u8865\u4e00\u4ef6\u8bb0\u5f97\u65f6\u95f4\u7684\u611f\u60c5\u6216\u5173\u7cfb\u53d8\u5316\u3002",
+ "unique_minute_claim": false
+ },
+ "oos_blind_prompts": [
+ {
+ "domain": "relationship",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u611f\u60c5\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5173\u7cfb\u53d8\u5316\uff1f",
+ "used_for_scoring": false
+ },
+ {
+ "domain": "family",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u5bb6\u4eba\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5bb6\u4eba\u53d8\u5316\uff1f",
+ "used_for_scoring": false
+ },
+ {
+ "domain": "finance",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u8d22\u52a1\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u6536\u5165\u6216\u8d44\u4ea7\u53d8\u5316\uff1f",
+ "used_for_scoring": false
+ }
+ ],
+ "discriminating_event_probes": [],
+ "guided_collect_windows": [
+ {
+ "year": 2026,
+ "month_lo": 1,
+ "month_hi": 1,
+ "domain": "any",
+ "split": {
+ "left": 1,
+ "right": 2
+ }
+ },
+ {
+ "year": 2026,
+ "month_lo": 3,
+ "month_hi": 3,
+ "domain": "any",
+ "split": {
+ "left": 1,
+ "right": 2
+ }
+ },
+ {
+ "year": 2026,
+ "month_lo": 9,
+ "month_hi": 9,
+ "domain": "any",
+ "split": {
+ "left": 2,
+ "right": 1
+ }
+ },
+ {
+ "year": 2026,
+ "month_lo": 9,
+ "month_hi": 9,
+ "domain": "relationship",
+ "split": {
+ "left": 1,
+ "right": 2
+ }
+ },
+ {
+ "year": 2026,
+ "month_lo": 12,
+ "month_hi": 12,
+ "domain": "relationship",
+ "split": {
+ "left": 1,
+ "right": 2
+ }
+ },
+ {
+ "year": 2025,
+ "month_lo": 2,
+ "month_hi": 2,
+ "domain": "any",
+ "split": {
+ "left": 2,
+ "right": 1
+ }
+ }
+ ],
+ "event_clarification_probes": [
+ {
+ "year": 2008,
+ "year_label": "2008 \u5e74\u524d\u540e",
+ "domain": "education",
+ "event_family": "\u5b66\u4e1a\u6216\u8003\u8bd5\u53d1\u6325\u5931\u5e38\u3001\u538b\u529b\u7279\u522b\u5927",
+ "source": "known_event_quality",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": true,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2008 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a education\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5b66\u4e1a\u6216\u8003\u8bd5\u53d1\u6325\u5931\u5e38\u3001\u538b\u529b\u7279\u522b\u5927\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\u6765\u6838\u5bf9\u4f53\u9a8c\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u5806\u53e0\u4f8b\u5b50\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "clarify",
+ "phase": "event_clarification",
+ "semantic_key": "education.2008",
+ "information_gain": 0.0,
+ "candidate_split_hash": "education:2008",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "event_quality",
+ "style_options": [
+ {
+ "label": "\u53d1\u6325\u660e\u663e\u5931\u5e38\u6216\u538b\u529b\u5f88\u5927",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u6709\u538b\u529b\u4f46\u4e0d\u7b97\u660e\u663e\u5931\u5e38",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u53d1\u6325\u6b63\u5e38\u3001\u6ca1\u6709\u660e\u663e\u5931\u5e38",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ }
+ ],
+ "evidence_collection_probes": [
+ {
+ "year": 2011,
+ "year_label": "2011 \u5e74\u524d\u540e",
+ "domain": "relocation",
+ "event_family": "\u642c\u5bb6\u6216\u957f\u671f\u4f4f\u5230\u5916\u5730",
+ "source": "age_band",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": false,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2011 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a relocation\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u642c\u5bb6\u6216\u957f\u671f\u4f4f\u5230\u5916\u5730\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "collect",
+ "phase": "evidence_collection",
+ "semantic_key": "relocation.2011",
+ "information_gain": 0.0,
+ "candidate_split_hash": "relocation:2011",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "existence",
+ "style_options": [
+ {
+ "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ },
+ {
+ "year": 2013,
+ "year_label": "2013 \u5e74\u524d\u540e",
+ "domain": "relationship",
+ "event_family": "\u5f00\u59cb\u4e00\u6bb5\u8ba4\u771f\u5173\u7cfb\u3001\u5206\u624b\u6216\u7ed3\u5a5a",
+ "source": "age_band",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": false,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2013 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a relationship\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5f00\u59cb\u4e00\u6bb5\u8ba4\u771f\u5173\u7cfb\u3001\u5206\u624b\u6216\u7ed3\u5a5a\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "collect",
+ "phase": "evidence_collection",
+ "semantic_key": "relationship.2013",
+ "information_gain": 0.0,
+ "candidate_split_hash": "relationship:2013",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "existence",
+ "style_options": [
+ {
+ "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ },
+ {
+ "year": 2014,
+ "year_label": "2014 \u5e74\u524d\u540e",
+ "domain": "family",
+ "event_family": "\u5bb6\u4eba\u7ed3\u5a5a\u3001\u6dfb\u4e01\u6216\u4f4f\u9662",
+ "source": "age_band",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": false,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2014 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a family\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u5bb6\u4eba\u7ed3\u5a5a\u3001\u6dfb\u4e01\u6216\u4f4f\u9662\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "collect",
+ "phase": "evidence_collection",
+ "semantic_key": "family.2014",
+ "information_gain": 0.0,
+ "candidate_split_hash": "family:2014",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "existence",
+ "style_options": [
+ {
+ "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ },
+ {
+ "year": 2017,
+ "year_label": "2017 \u5e74\u524d\u540e",
+ "domain": "finance",
+ "event_family": "\u6536\u5165\u660e\u663e\u53d8\u5316\u3001\u5927\u7b14\u652f\u51fa\u6216\u6b20\u503a",
+ "source": "age_band",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": false,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2017 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a finance\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u6536\u5165\u660e\u663e\u53d8\u5316\u3001\u5927\u7b14\u652f\u51fa\u6216\u6b20\u503a\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "collect",
+ "phase": "evidence_collection",
+ "semantic_key": "finance.2017",
+ "information_gain": 0.0,
+ "candidate_split_hash": "finance:2017",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "existence",
+ "style_options": [
+ {
+ "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ },
+ {
+ "year": 2018,
+ "year_label": "2018 \u5e74\u524d\u540e",
+ "domain": "health_pressure",
+ "event_family": "\u751f\u75c5\u3001\u53d7\u4f24\u6216\u538b\u529b\u7279\u522b\u5927",
+ "source": "age_band",
+ "tracks": [
+ "vimshottari",
+ "narayana"
+ ],
+ "tracks_agree": false,
+ "unique_minute_claim": false,
+ "user_meaning": "\u65f6\u95f4\u8303\u56f4\u9501\u5b9a 2018 \u5e74\u524d\u540e\uff1b\u9886\u57df\u9501\u5b9a health_pressure\uff1b\u8bed\u4e49\u76ee\u6807\u662f \u751f\u75c5\u3001\u53d7\u4f24\u6216\u538b\u529b\u7279\u522b\u5927\u3002\u7ed3\u5408\u6700\u8fd1\u5bf9\u8bdd\uff0c\u53ea\u9009\u4e00\u4e2a\u5bb9\u6613\u56de\u7b54\u7684\u53e3\u8bed\u5165\u53e3\uff0c\u5199\u4e00\u53e5\u81ea\u7136\u7684\u662f/\u5426\u9898\uff1b\u4e0d\u8981\u9010\u5b57\u590d\u8ff0\u8bed\u4e49\u76ee\u6807\uff0c\u4e0d\u8981\u628a\u6240\u6709\u4f8b\u5b50\u5806\u8fdb\u4e00\u53e5\uff0c\u4e0d\u5f97\u6539\u65f6\u95f4\u8303\u56f4\u3002",
+ "role": "collect",
+ "phase": "evidence_collection",
+ "semantic_key": "health_pressure.2018",
+ "information_gain": 0.0,
+ "candidate_split_hash": "health_pressure:2018",
+ "expected_outcomes": [],
+ "candidate_ids": [],
+ "choice_kind": "existence",
+ "style_options": [
+ {
+ "label": "\u660e\u786e\u53d1\u751f\u4e14\u65f6\u95f4\u543b\u5408",
+ "answer_class": "yes"
+ },
+ {
+ "label": "\u53d1\u751f\u8fc7\u4f46\u7a0b\u5ea6\u8f83\u5f31",
+ "answer_class": "weak_yes"
+ },
+ {
+ "label": "\u660e\u786e\u6ca1\u6709\u53d1\u751f",
+ "answer_class": "no"
+ },
+ {
+ "label": "\u8fd9\u6bb5\u8bb0\u4e0d\u6e05\u695a",
+ "answer_class": "unsure"
+ }
+ ],
+ "question_contract_version": "probe-question-v1"
+ }
+ ],
+ "candidate_contrast_opportunities": [],
+ "holdout_validation_probes": [
+ {
+ "domain": "relationship",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u611f\u60c5\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5173\u7cfb\u53d8\u5316\uff1f",
+ "used_for_scoring": false,
+ "phase": "holdout_validation",
+ "role": "holdout",
+ "source": "oos_blind"
+ },
+ {
+ "domain": "family",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u5bb6\u4eba\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u5bb6\u4eba\u53d8\u5316\uff1f",
+ "used_for_scoring": false,
+ "phase": "holdout_validation",
+ "role": "holdout",
+ "source": "oos_blind"
+ },
+ {
+ "domain": "finance",
+ "user_meaning": "\u6821\u65f6\u8fd8\u6ca1\u7528\u8fc7\u8d22\u52a1\u8fd9\u6761\u7ebf\u3002\u6709\u6ca1\u6709\u4e00\u4ef6\u6ca1\u63d0\u8fc7\u3001\u4f46\u8bb0\u5f97\u5927\u6982\u65f6\u95f4\u7684\u6536\u5165\u6216\u8d44\u4ea7\u53d8\u5316\uff1f",
+ "used_for_scoring": false,
+ "phase": "holdout_validation",
+ "role": "holdout",
+ "source": "oos_blind"
+ }
+ ],
+ "dropped_probes": [],
+ "dropped_asked_probe_keys": 0,
+ "prospective_probes": [
+ {
+ "candidate_label": "A",
+ "domain": "career",
+ "window_label": "2027 \u5e74 1 \u6708\u9644\u8fd1",
+ "user_meaning": "\u5019\u9009 A \u9884\u6d4b\u4e0b\u4e00\u6b21\u4e8b\u4e1a\u53d8\u52a8\u66f4\u53ef\u80fd\u5728 2027 \u5e74 1 \u6708\u9644\u8fd1\u3002\u8fd9\u662f\u9884\u6d4b\u7a97\u53e3\uff0c\u4e0d\u662f\u627f\u8bfa\uff1b\u4e0b\u6b21\u53d1\u751f\u65f6\u56de\u6765\u8865\u4e00\u6761\uff0c\u53ef\u8fdb\u4e00\u6b65\u5206\u8fa8\u3002",
+ "used_for_scoring": false
+ },
+ {
+ "candidate_label": "B",
+ "domain": "career",
+ "window_label": "2027 \u5e74 1 \u6708\u9644\u8fd1",
+ "user_meaning": "\u5019\u9009 B \u9884\u6d4b\u4e0b\u4e00\u6b21\u4e8b\u4e1a\u53d8\u52a8\u66f4\u53ef\u80fd\u5728 2027 \u5e74 1 \u6708\u9644\u8fd1\u3002\u8fd9\u662f\u9884\u6d4b\u7a97\u53e3\uff0c\u4e0d\u662f\u627f\u8bfa\uff1b\u4e0b\u6b21\u53d1\u751f\u65f6\u56de\u6765\u8865\u4e00\u6761\uff0c\u53ef\u8fdb\u4e00\u6b65\u5206\u8fa8\u3002",
+ "used_for_scoring": false
+ }
+ ],
+ "horary_observation": {
+ "status": "skipped",
+ "user_meaning": "\u6ca1\u6709\u7b2c\u4e00\u6b21\u95ee\u8d77\u8fd9\u4ef6\u4e8b\u7684\u65f6\u95f4\uff0c\u5360\u95ee\u89c2\u5bdf\u672a\u6267\u884c\u3002",
+ "unique_minute_claim": false
+ },
+ "unique_minute_claim": false,
+ "candidate_window_contract": "dated-v1",
+ "candidate_intervals": [
+ {
+ "start_at": "1990-01-01T12:00",
+ "end_at": "1990-01-01T12:02"
+ }
+ ],
+ "candidate_timezone_offset": 8.0,
+ "candidate_timezone_id": ""
+ },
+ "candidate_feature_snapshot": {
+ "calculation_spec_hash": "e03dfc555a247e78549768cc0b487cb427414633982212debd9cd6e60bb67292",
+ "algorithm_version": "rectification-v5-matrix-scoring-10",
+ "candidate_count": 3,
+ "feature_hash": "05c6ae5bc91e0383d30c8906f92731a8da7b04b092638c8baf0dfbb80d828006",
+ "features": [
+ {
+ "time": "12:00",
+ "ascendant_degree": 349.5527,
+ "ascendant_sign_index": 11,
+ "pada_index": 104,
+ "bhava_sign_index": 10,
+ "hora_sign_index": 1,
+ "ghati_sign_index": 1,
+ "pranapada_sign_index": 2,
+ "kp1_sub_index": 8,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8,
+ "kp_cusps": {
+ "status": "executed",
+ "house_system": "placidus",
+ "ayanamsa": "krishnamurti",
+ "houses": {
+ "1": {
+ "cusp_degree": 348.2035,
+ "sign": "Pisces",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Revati",
+ "nakshatra_lord": "Mercury",
+ "pada": 1,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 8
+ },
+ "2": {
+ "cusp_degree": 26.072,
+ "sign": "Aries",
+ "rasi_lord": "Mars",
+ "nakshatra": "Bharani",
+ "nakshatra_lord": "Venus",
+ "pada": 4,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Sun",
+ "sub_index": 0
+ },
+ "3": {
+ "cusp_degree": 51.3992,
+ "sign": "Taurus",
+ "rasi_lord": "Venus",
+ "nakshatra": "Rohini",
+ "nakshatra_lord": "Moon",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Rahu",
+ "sub_index": 1
+ },
+ "4": {
+ "cusp_degree": 72.7617,
+ "sign": "Gemini",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Ardra",
+ "nakshatra_lord": "Rahu",
+ "pada": 2,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Mercury",
+ "sub_index": 8
+ },
+ "5": {
+ "cusp_degree": 95.1596,
+ "sign": "Cancer",
+ "rasi_lord": "Moon",
+ "nakshatra": "Pushya",
+ "nakshatra_lord": "Saturn",
+ "pada": 1,
+ "sub_lord": "Saturn",
+ "sub_sub_lord": "Rahu",
+ "sub_index": 7
+ },
+ "6": {
+ "cusp_degree": 124.1819,
+ "sign": "Leo",
+ "rasi_lord": "Sun",
+ "nakshatra": "Magha",
+ "nakshatra_lord": "Ketu",
+ "pada": 2,
+ "sub_lord": "Moon",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 3
+ },
+ "7": {
+ "cusp_degree": 168.2035,
+ "sign": "Virgo",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Hasta",
+ "nakshatra_lord": "Moon",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Venus",
+ "sub_index": 8
+ },
+ "8": {
+ "cusp_degree": 206.072,
+ "sign": "Libra",
+ "rasi_lord": "Venus",
+ "nakshatra": "Vishakha",
+ "nakshatra_lord": "Jupiter",
+ "pada": 2,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Mars",
+ "sub_index": 0
+ },
+ "9": {
+ "cusp_degree": 231.3992,
+ "sign": "Scorpio",
+ "rasi_lord": "Mars",
+ "nakshatra": "Jyeshtha",
+ "nakshatra_lord": "Mercury",
+ "pada": 2,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Mercury",
+ "sub_index": 1
+ },
+ "10": {
+ "cusp_degree": 252.7617,
+ "sign": "Sagittarius",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Mula",
+ "nakshatra_lord": "Ketu",
+ "pada": 4,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Rahu",
+ "sub_index": 8
+ },
+ "11": {
+ "cusp_degree": 275.1596,
+ "sign": "Capricorn",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Uttara Ashadha",
+ "nakshatra_lord": "Sun",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Mercury",
+ "sub_index": 8
+ },
+ "12": {
+ "cusp_degree": 304.1819,
+ "sign": "Aquarius",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Dhanishta",
+ "nakshatra_lord": "Mars",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 1
+ }
+ },
+ "indices": {
+ "kp1_sub_index": 8,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8
+ },
+ "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002"
+ },
+ "varga_ascendants": {
+ "D2": 4,
+ "D3": 3,
+ "D4": 5,
+ "D5": 10,
+ "D7": 9,
+ "D9": 8,
+ "D10": 1,
+ "D11": 2,
+ "D12": 6,
+ "D24": 6,
+ "D30": 9
+ },
+ "arudha_signs": {
+ "A7": 1,
+ "A10": 5,
+ "UL": 6
+ },
+ "available_layers": [
+ "A10",
+ "A7",
+ "Ashtakavarga",
+ "D1",
+ "D10",
+ "D11",
+ "D12",
+ "D2",
+ "D24",
+ "D3",
+ "D30",
+ "D4",
+ "D5",
+ "D7",
+ "D9",
+ "KP_cusps",
+ "Shadbala",
+ "UL"
+ ],
+ "blocked_layers": [],
+ "fingerprints": {
+ "natal": "4e8c7a9f3654718cbb939d8f2573b2683e23bd4ec9371a0a9e789011b90fb6bb",
+ "vargas": "e0b1798bb91fef314c36311fb612af9c6943a8c0b57a60ae68e681a09a49abb9",
+ "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0",
+ "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1",
+ "shadbala": "80aed631b44e127d3c810748636f6bb0abca18cf185d34610c3d8751df45e6dd",
+ "static": "d246d8026b5cac65a48a5a3ef71d4291e661651721149d05030818f73f2062b5"
+ }
+ },
+ {
+ "time": "12:01",
+ "ascendant_degree": 349.9746,
+ "ascendant_sign_index": 11,
+ "pada_index": 104,
+ "bhava_sign_index": 10,
+ "hora_sign_index": 1,
+ "ghati_sign_index": 1,
+ "pranapada_sign_index": 2,
+ "kp1_sub_index": 0,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8,
+ "kp_cusps": {
+ "status": "executed",
+ "house_system": "placidus",
+ "ayanamsa": "krishnamurti",
+ "houses": {
+ "1": {
+ "cusp_degree": 348.625,
+ "sign": "Pisces",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Revati",
+ "nakshatra_lord": "Mercury",
+ "pada": 1,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Venus",
+ "sub_index": 0
+ },
+ "2": {
+ "cusp_degree": 26.3705,
+ "sign": "Aries",
+ "rasi_lord": "Mars",
+ "nakshatra": "Bharani",
+ "nakshatra_lord": "Venus",
+ "pada": 4,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 0
+ },
+ "3": {
+ "cusp_degree": 51.6406,
+ "sign": "Taurus",
+ "rasi_lord": "Venus",
+ "nakshatra": "Rohini",
+ "nakshatra_lord": "Moon",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 1
+ },
+ "4": {
+ "cusp_degree": 72.9921,
+ "sign": "Gemini",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Ardra",
+ "nakshatra_lord": "Rahu",
+ "pada": 2,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Venus",
+ "sub_index": 8
+ },
+ "5": {
+ "cusp_degree": 95.4163,
+ "sign": "Cancer",
+ "rasi_lord": "Moon",
+ "nakshatra": "Pushya",
+ "nakshatra_lord": "Saturn",
+ "pada": 1,
+ "sub_lord": "Saturn",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 7
+ },
+ "6": {
+ "cusp_degree": 124.5135,
+ "sign": "Leo",
+ "rasi_lord": "Sun",
+ "nakshatra": "Magha",
+ "nakshatra_lord": "Ketu",
+ "pada": 2,
+ "sub_lord": "Moon",
+ "sub_sub_lord": "Ketu",
+ "sub_index": 3
+ },
+ "7": {
+ "cusp_degree": 168.625,
+ "sign": "Virgo",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Hasta",
+ "nakshatra_lord": "Moon",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Moon",
+ "sub_index": 8
+ },
+ "8": {
+ "cusp_degree": 206.3705,
+ "sign": "Libra",
+ "rasi_lord": "Venus",
+ "nakshatra": "Vishakha",
+ "nakshatra_lord": "Jupiter",
+ "pada": 2,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 0
+ },
+ "9": {
+ "cusp_degree": 231.6406,
+ "sign": "Scorpio",
+ "rasi_lord": "Mars",
+ "nakshatra": "Jyeshtha",
+ "nakshatra_lord": "Mercury",
+ "pada": 2,
+ "sub_lord": "Sun",
+ "sub_sub_lord": "Moon",
+ "sub_index": 2
+ },
+ "10": {
+ "cusp_degree": 252.9921,
+ "sign": "Sagittarius",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Mula",
+ "nakshatra_lord": "Ketu",
+ "pada": 4,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 8
+ },
+ "11": {
+ "cusp_degree": 275.4163,
+ "sign": "Capricorn",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Uttara Ashadha",
+ "nakshatra_lord": "Sun",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Ketu",
+ "sub_index": 8
+ },
+ "12": {
+ "cusp_degree": 304.5135,
+ "sign": "Aquarius",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Dhanishta",
+ "nakshatra_lord": "Mars",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Mercury",
+ "sub_index": 1
+ }
+ },
+ "indices": {
+ "kp1_sub_index": 0,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8
+ },
+ "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002"
+ },
+ "varga_ascendants": {
+ "D2": 4,
+ "D3": 3,
+ "D4": 5,
+ "D5": 10,
+ "D7": 9,
+ "D9": 8,
+ "D10": 1,
+ "D11": 2,
+ "D12": 6,
+ "D24": 6,
+ "D30": 9
+ },
+ "arudha_signs": {
+ "A7": 1,
+ "A10": 5,
+ "UL": 6
+ },
+ "available_layers": [
+ "A10",
+ "A7",
+ "Ashtakavarga",
+ "D1",
+ "D10",
+ "D11",
+ "D12",
+ "D2",
+ "D24",
+ "D3",
+ "D30",
+ "D4",
+ "D5",
+ "D7",
+ "D9",
+ "KP_cusps",
+ "Shadbala",
+ "UL"
+ ],
+ "blocked_layers": [],
+ "fingerprints": {
+ "natal": "fc03340c27facfe96e961087d20c5a479ef159af1aec870f2a3ab3ca5e9b5ec5",
+ "vargas": "e0b1798bb91fef314c36311fb612af9c6943a8c0b57a60ae68e681a09a49abb9",
+ "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0",
+ "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1",
+ "shadbala": "2cc4f802fb6087b3c9d9dbac33bfc2efec97b5d3f395583f7fddff9cee2ea79f",
+ "static": "1ec2832d8361f8bc8da0cfb9d7921fd6bad430b05048f896aaf9b28cd6958d7a"
+ }
+ },
+ {
+ "time": "12:02",
+ "ascendant_degree": 350.3959,
+ "ascendant_sign_index": 11,
+ "pada_index": 105,
+ "bhava_sign_index": 10,
+ "hora_sign_index": 1,
+ "ghati_sign_index": 1,
+ "pranapada_sign_index": 2,
+ "kp1_sub_index": 0,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8,
+ "kp_cusps": {
+ "status": "executed",
+ "house_system": "placidus",
+ "ayanamsa": "krishnamurti",
+ "houses": {
+ "1": {
+ "cusp_degree": 349.0467,
+ "sign": "Pisces",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Revati",
+ "nakshatra_lord": "Mercury",
+ "pada": 1,
+ "sub_lord": "Ketu",
+ "sub_sub_lord": "Jupiter",
+ "sub_index": 0
+ },
+ "2": {
+ "cusp_degree": 26.6689,
+ "sign": "Aries",
+ "rasi_lord": "Mars",
+ "nakshatra": "Krittika",
+ "nakshatra_lord": "Sun",
+ "pada": 1,
+ "sub_lord": "Sun",
+ "sub_sub_lord": "Sun",
+ "sub_index": 2
+ },
+ "3": {
+ "cusp_degree": 51.882,
+ "sign": "Taurus",
+ "rasi_lord": "Venus",
+ "nakshatra": "Rohini",
+ "nakshatra_lord": "Moon",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 1
+ },
+ "4": {
+ "cusp_degree": 73.2229,
+ "sign": "Gemini",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Ardra",
+ "nakshatra_lord": "Rahu",
+ "pada": 2,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Venus",
+ "sub_index": 8
+ },
+ "5": {
+ "cusp_degree": 95.6738,
+ "sign": "Cancer",
+ "rasi_lord": "Moon",
+ "nakshatra": "Pushya",
+ "nakshatra_lord": "Saturn",
+ "pada": 1,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Mercury",
+ "sub_index": 8
+ },
+ "6": {
+ "cusp_degree": 124.8461,
+ "sign": "Leo",
+ "rasi_lord": "Sun",
+ "nakshatra": "Magha",
+ "nakshatra_lord": "Ketu",
+ "pada": 2,
+ "sub_lord": "Mars",
+ "sub_sub_lord": "Rahu",
+ "sub_index": 4
+ },
+ "7": {
+ "cusp_degree": 169.0467,
+ "sign": "Virgo",
+ "rasi_lord": "Mercury",
+ "nakshatra": "Hasta",
+ "nakshatra_lord": "Moon",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Rahu",
+ "sub_index": 8
+ },
+ "8": {
+ "cusp_degree": 206.6689,
+ "sign": "Libra",
+ "rasi_lord": "Venus",
+ "nakshatra": "Vishakha",
+ "nakshatra_lord": "Jupiter",
+ "pada": 3,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Venus",
+ "sub_index": 1
+ },
+ "9": {
+ "cusp_degree": 231.882,
+ "sign": "Scorpio",
+ "rasi_lord": "Mars",
+ "nakshatra": "Jyeshtha",
+ "nakshatra_lord": "Mercury",
+ "pada": 2,
+ "sub_lord": "Sun",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 2
+ },
+ "10": {
+ "cusp_degree": 253.2229,
+ "sign": "Sagittarius",
+ "rasi_lord": "Jupiter",
+ "nakshatra": "Mula",
+ "nakshatra_lord": "Ketu",
+ "pada": 4,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Saturn",
+ "sub_index": 8
+ },
+ "11": {
+ "cusp_degree": 275.6738,
+ "sign": "Capricorn",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Uttara Ashadha",
+ "nakshatra_lord": "Sun",
+ "pada": 3,
+ "sub_lord": "Mercury",
+ "sub_sub_lord": "Venus",
+ "sub_index": 8
+ },
+ "12": {
+ "cusp_degree": 304.8461,
+ "sign": "Aquarius",
+ "rasi_lord": "Saturn",
+ "nakshatra": "Dhanishta",
+ "nakshatra_lord": "Mars",
+ "pada": 4,
+ "sub_lord": "Venus",
+ "sub_sub_lord": "Ketu",
+ "sub_index": 1
+ }
+ },
+ "indices": {
+ "kp1_sub_index": 0,
+ "kp4_sub_index": 8,
+ "kp7_sub_index": 8,
+ "kp10_sub_index": 8
+ },
+ "note": "\u5df2\u6309 Swiss Ephemeris Placidus + Krishnamurti \u89c2\u5bdf 12 \u5bab\u5934\uff1b\u4e0d\u8ba1\u5206\uff0c\u4e0d\u53c2\u4e0e\u63d0\u51fa\u95e8\u6216\u786e\u8ba4\u95e8\u3002"
+ },
+ "varga_ascendants": {
+ "D2": 4,
+ "D3": 7,
+ "D4": 5,
+ "D5": 10,
+ "D7": 9,
+ "D9": 9,
+ "D10": 1,
+ "D11": 2,
+ "D12": 7,
+ "D24": 7,
+ "D30": 7
+ },
+ "arudha_signs": {
+ "A7": 1,
+ "A10": 5,
+ "UL": 6
+ },
+ "available_layers": [
+ "A10",
+ "A7",
+ "Ashtakavarga",
+ "D1",
+ "D10",
+ "D11",
+ "D12",
+ "D2",
+ "D24",
+ "D3",
+ "D30",
+ "D4",
+ "D5",
+ "D7",
+ "D9",
+ "KP_cusps",
+ "Shadbala",
+ "UL"
+ ],
+ "blocked_layers": [],
+ "fingerprints": {
+ "natal": "87c2fd4ce5a283b332f7b97caaa6edb29d6ee11e05a419a4a214933f5b7179df",
+ "vargas": "75d7881b48fb899ee7d0902c42e3bc878ba23e92efee34449563c5ce50286c8a",
+ "arudha": "134795db88292046a494171e37fc7b5867546ebbed3f814f0cd919ddd42e8ec0",
+ "ashtakavarga": "5131abf66bb1d5ff6ab7cec48d35b21665837aa23ff65baa91a08cf809e0d6f1",
+ "shadbala": "9c443d93db24f3f91aea16b1e62669851091b31010dfc40f6a4b2ff7133f21f7",
+ "static": "ea8d1326e9488fb9ab6eaf24cb2c714de0650f96a1afa44bb444fda4010981f5"
+ }
+ }
+ ]
+ }
+}
diff --git a/tests/test_rectification_confirmation_and.py b/tests/test_rectification_confirmation_and.py
index 751fd416..b1a4bc34 100644
--- a/tests/test_rectification_confirmation_and.py
+++ b/tests/test_rectification_confirmation_and.py
@@ -226,7 +226,7 @@ def test_kp_only_missing_does_not_block_propose_or_engine_grant() -> None:
assert "missing_mandatory_layers" not in receipt["confirmation_reasons"]
-def test_four_events_two_domains_selects_but_does_not_propose() -> None:
+def test_four_events_two_domains_neither_selects_nor_proposes() -> None:
events = [
{
"id": "00000000-0000-4000-8000-000000000001",
@@ -277,7 +277,14 @@ def test_four_events_two_domains_selects_but_does_not_propose() -> None:
"primary_secondary_margin_percent": 10.27,
},
)
- assert receipt["selection_allowed"] is True
+ # 原值: selection_allowed=True(区间门槛 3 件 2 域,4 件 2 域可选不可提议)
+ # 新值: selection_allowed=False,domain_diversity 不通过,原因 insufficient_domain_diversity
+ # 原因: R1 生时校正交付门槛 4 件 3 域(BUG-1193),2 个领域不再给区间
+ assert receipt["selection_allowed"] is False
+ assert receipt["gates"]["event_quality"]["passed"] is True
+ assert receipt["gates"]["domain_diversity"]["passed"] is False
+ assert receipt["gates"]["domain_diversity"]["minimum"] == 3
+ assert "insufficient_domain_diversity" in receipt["reasons"]
assert receipt["propose_allowed"] is False
assert receipt["gates"]["exact_confirmation"]["engine_granted"] is False
assert "insufficient_confirmation_domains" in receipt["confirmation_reasons"]
diff --git a/tests/test_rectification_engine_memoization.py b/tests/test_rectification_engine_memoization.py
index a5efeb1a..419e1623 100644
--- a/tests/test_rectification_engine_memoization.py
+++ b/tests/test_rectification_engine_memoization.py
@@ -20,6 +20,16 @@ input (candidate 12:00: 8.6274 -> 8.4977), which is why the algorithm identity
was bumped; it already carries the dated-v1 receipt metadata. Never rewrite it
in place: a future scoring change adds a v3 file and freezes this one.
+Current golden tests/golden/rectification_engine_memoization_v3.json
+(2026-10-02, BUG-1193, upstream sync 5) was written by ``write_golden`` after
+R1 raised the range floor to 4 dated events / 3 domains. Candidate scores are
+identical to v2 (the ported combustion / Chara Karaka / Yogini / Shodhana
+changes do not feed the event matrix; 77 v5 cases A/B byte-identical), so the
+algorithm stays scoring-10. Only the decision receipt moved: policy
+rectification-candidate-policy-v3 -> v4, event_quality / domain_diversity
+minimum 3/2 -> 4/3, and the representative candidate UUID (its namespace
+carries the policy version). v2 is frozen by sha256 below.
+
Do not compare that payload with a whole-structure ``==``. Cross-machine
libm / pyswisseph rounding already drifted ``margin_percent`` by 1.1e-3
(TASK-rectification-engine-memoization-fix-20260915). Equivalence of the
@@ -58,15 +68,20 @@ import scripts.rectification.scoring_service as scoring_service
import shadbala
ROOT = Path(__file__).resolve().parents[1]
-GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v2.json"
+GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v3.json"
+PREVIOUS_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v2.json"
+PREVIOUS_GOLDEN_SHA256 = "abb15efae4d86387d930cd463b82a22fe95ccd1779e85ca6cc4cdf07a063cb73"
+PREVIOUS_SOURCE_COMMIT = "57782aea8ae28f5dd165a08d221ec27dbef391f8"
HISTORICAL_GOLDEN_PATH = ROOT / "tests" / "golden" / "rectification_engine_memoization_v1.json"
HISTORICAL_GOLDEN_SHA256 = "6266e448d7dad204b264cbe8f9bcf4c36e02286794764a94edb9e2ce4d0e3015"
CURRENT_ALGORITHM_VERSION = "rectification-v5-matrix-scoring-10"
FROZEN_TODAY = date(2026, 9, 16)
TIMING_KEYS = frozenset({"column_compare_ms"})
HISTORICAL_SOURCE_COMMIT = "a8d29d1b6cc37ff865ddec6c8bccdf9aa889ee53"
-# Engine code of the v2 golden; the scoring-10 label is the BUG-1181 commit on top.
-SOURCE_COMMIT = "57782aea8ae28f5dd165a08d221ec27dbef391f8"
+CURRENT_POLICY_VERSION = "rectification-candidate-policy-v4"
+# Engine code of the v3 golden: upstream-sync5 tip before R1; the policy-v4
+# receipt (R1, BUG-1193) is the commit that adds the v3 file on top.
+SOURCE_COMMIT = "7fb165ecf66ba1726f2fc74ffa2353fadc65ba39"
CACHE_LAYER_KEYS = (
"ashtakavarga_result",
"shadbala_result",
@@ -250,7 +265,7 @@ def _contexts_with_layer_cache_cleared(contexts: list[dict[str, Any]]) -> list[d
def _assert_dated_payload_against_golden(actual: dict[str, Any], expected: dict[str, Any]) -> None:
- """Compare with the scoring-10 golden; identity is spelled out, not trusted."""
+ """Compare with the scoring-10 / policy-v4 golden; identity is spelled out, not trusted."""
metadata = {
"candidate_window_contract": "dated-v1",
"candidate_intervals": [{"start_at": "1990-01-01T12:00", "end_at": "1990-01-01T12:02"}],
@@ -276,7 +291,7 @@ def _assert_dated_payload_against_golden(actual: dict[str, Any], expected: dict[
).encode()).hexdigest()
result_id = uuid5(NAMESPACE_URL, f"{CURRENT_ALGORITHM_VERSION}:{fingerprint}")
representative_time = golden_receipt["representative_time"]
- candidate_id = uuid5(NAMESPACE_URL, f"rectification-candidate-policy-v3:{result_id}:{representative_time}")
+ candidate_id = uuid5(NAMESPACE_URL, f"{CURRENT_POLICY_VERSION}:{result_id}:{representative_time}")
assert golden_receipt["representative_candidate_id"] == str(candidate_id)
assert receipt["representative_candidate_id"] == str(candidate_id)
# BUG-733/985: fingerprints hash unrounded floats; do not turn the
@@ -297,6 +312,26 @@ def test_historical_scoring_8_golden_is_frozen_and_no_longer_current() -> None:
_assert_memoization_payloads(current, historical)
+def test_previous_policy_v3_golden_is_frozen_with_identical_scores() -> None:
+ """BUG-1193: v2 stays byte-identical; scores equal v3 (no scoring-11), only the receipt policy moved."""
+ raw = PREVIOUS_GOLDEN_PATH.read_bytes()
+ assert hashlib.sha256(raw).hexdigest() == PREVIOUS_GOLDEN_SHA256
+ previous = json.loads(raw)
+ current = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
+ assert previous["source_commit"] == PREVIOUS_SOURCE_COMMIT
+ assert previous["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
+ assert current["candidate_feature_snapshot"]["algorithm_version"] == CURRENT_ALGORITHM_VERSION
+ _assert_tiered_equal(current["candidate_scores"], previous["candidate_scores"], path="candidate_scores")
+ assert previous["decision_receipt"]["policy_version"] == "rectification-candidate-policy-v3"
+ assert current["decision_receipt"]["policy_version"] == CURRENT_POLICY_VERSION
+ assert previous["decision_receipt"]["gates"]["event_quality"]["minimum"] == 3
+ assert current["decision_receipt"]["gates"]["event_quality"]["minimum"] == 4
+ assert previous["decision_receipt"]["gates"]["domain_diversity"]["minimum"] == 2
+ assert current["decision_receipt"]["gates"]["domain_diversity"]["minimum"] == 3
+ with pytest.raises(AssertionError):
+ _assert_memoization_payloads(current, previous)
+
+
def test_score_candidates_matches_baseline_golden() -> None:
expected = json.loads(GOLDEN_PATH.read_text(encoding="utf-8"))
actual = json.loads(json.dumps(_golden_payload(), ensure_ascii=True))
diff --git a/tests/test_rectification_relative_support.py b/tests/test_rectification_relative_support.py
index 2af7173c..06fb1a3d 100644
--- a/tests/test_rectification_relative_support.py
+++ b/tests/test_rectification_relative_support.py
@@ -18,7 +18,8 @@ def _row(time: str, score: float) -> dict:
class RelativeSupportScaleTest(unittest.TestCase):
def test_holdout_gate_keeps_proportional_default(self) -> None:
- self.assertEqual(POLICY_VERSION, "rectification-candidate-policy-v3")
+ # 原值: policy-v3;新值: policy-v4;原因: R1 区间门槛 4 件 3 域(BUG-1193),相对支持口径不变。
+ self.assertEqual(POLICY_VERSION, "rectification-candidate-policy-v4")
# Explicit date windows: scoring identity -8 -> -9, not a prior/policy change.
# 原值: -9;新值: -10;原因: 功能吉凶 v2 改变打分(BUG-1181),策略 v3 与相对支持口径不变。
self.assertEqual(ALGORITHM_VERSION, "rectification-v5-matrix-scoring-10")
diff --git a/tests/test_rectification_v5_services.py b/tests/test_rectification_v5_services.py
index 36d9b143..7c8cb82e 100644
--- a/tests/test_rectification_v5_services.py
+++ b/tests/test_rectification_v5_services.py
@@ -503,7 +503,8 @@ class RectificationV5ServicesTest(unittest.TestCase):
self.assertEqual(first["decision_receipt"], receipt)
# C2 holdout 校准:offset/softmax 覆盖下降,默认仍用 proportional,policy 不 bump
- self.assertEqual(first["decision_policy_version"], "rectification-candidate-policy-v3")
+ # 原值: policy-v3;新值: policy-v4;原因: R1 区间门槛 3 件 2 域 → 4 件 3 域(BUG-1193),policy 身份随门槛 bump
+ self.assertEqual(first["decision_policy_version"], "rectification-candidate-policy-v4")
self.assertTrue(receipt["display_allowed"])
self.assertTrue(receipt["accept_allowed"])
self.assertFalse(receipt["confirm_allowed"])