From 758fee954b4eb965676c960b7bf40189382ee63d Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 3 Oct 2026 00:08:00 +0800 Subject: [PATCH] feat(rectification): range delivery needs 4 dated events across 3 domains (R1, BUG-1193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Product decision 2026-10-02 (TASK-upstream-sync5 R1): a time range is offered only with at least 4 dated, primary-scoreable events covering 3 domains, counted on all of them (training + reserved holdout). Was 3 training events / 2 domains in three TS copies and the Python acceptance gate while the policy file already said 4/3. - One definition: references/rectification_policy.v1.json (minConfirmationEvents / minConfirmationDomains). TS core/types MIN_DATED_*, rectification-decision MIN_STANDALONE_*, evidence-model MIN_ACCEPTANCE_*, the convergence evaluator and the post-inference trainingGateOpen all read it; Python decision_policy MIN_ACCEPTANCE_* alias MIN_CONFIRMATION_*. - Python receipt counts all scoreable events / domains for event_quality and domain_diversity; decision policy identity v3 -> v4 (candidate UUIDs carry it). Candidate scores unchanged (77 v5 cases A/B identical), so the algorithm stays rectification-v5-matrix-scoring-10. - Memoization golden v3 written by write_golden; v2 frozen by sha256 with a test that its scores equal v3 and only the receipt policy moved. - Collect gap copy names the exact gap ("再来两件……其中至少一件不是……") instead of always "再来一件"; VOICE.md updated. Legacy life-events form copy 4/3 as well. - 30 frontend test files, 4 Python tests: fixtures extended to the same scenario at 4/3, or assertions changed with 原值/新值/原因 notes. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- frontend/docs/VOICE.md | 2 +- .../src/components/birth-time-life-events.tsx | 11 +- .../core/convergence-evaluator.ts | 8 +- .../core/rectification-decision.ts | 8 +- .../lib/rectification-agentic/core/types.ts | 11 +- .../v9/collection-question-pool.ts | 44 +- .../v9/decision-from-dossier.ts | 11 +- .../v9/evidence-model.ts | 23 +- ...tification-adopt-flow-fix-20260903.test.ts | 12 + .../tests/rectification-answer-choice.test.ts | 23 + .../rectification-block-scan-20260906.test.ts | 18 +- .../tests/rectification-collect-stall.test.ts | 34 +- ...ification-collection-question-pool.test.ts | 12 +- .../rectification-convergence-budget.test.ts | 46 +- ...tification-dead-d9-choice-20260916.test.ts | 8 + .../rectification-decide-next-action.test.ts | 18 +- .../rectification-decision-authority.test.ts | 12 +- ...ation-delivery-vs-collect-20260914.test.ts | 3 +- ...discriminator-followup-consistency.test.ts | 14 + .../tests/rectification-eight-method.test.ts | 37 +- ...ication-fewer-probes-card-20260926.test.ts | 3 +- ...ation-futile-collect-stop-20260929.test.ts | 4 +- ...n-occupation-dated-answer-20260911.test.ts | 17 +- ...ation-open-collect-invite-20260914.test.ts | 13 + ...tion-probe-pool-exhausted-20260911.test.ts | 18 +- .../rectification-range-offer-deadend.test.ts | 13 + ...ification-tie-break-entry-20260913.test.ts | 10 + ...tion-tiebreak-before-card-20260914.test.ts | 25 +- ...tification-tied-first-fix-20260914.test.ts | 8 +- ...ication-unstampable-probe-20260914.test.ts | 11 + frontend/tests/rectification-v9-agent.test.ts | 15 + scripts/rectification/decision_policy.py | 21 +- .../rectification_engine_memoization_v3.json | 1792 +++++++++++++++++ tests/test_rectification_confirmation_and.py | 11 +- .../test_rectification_engine_memoization.py | 45 +- tests/test_rectification_relative_support.py | 3 +- tests/test_rectification_v5_services.py | 3 +- 37 files changed, 2270 insertions(+), 97 deletions(-) create mode 100644 tests/golden/rectification_engine_memoization_v3.json 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"])