feat(rectification): range delivery needs 4 dated events across 3 domains (R1, BUG-1193)

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-03 00:08:00 +08:00
co-authored by Claude Opus 5.5
parent 7fb165ecf6
commit 758fee954b
37 changed files with 2270 additions and 97 deletions
@@ -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({
<span>{drafts.length} / 6</span>
</div>
<p className="birth-time-evidence-note">
请填写至少三条、覆盖两个领域的经历。只使用日期和类型评分,不会从描述中猜测。
请填写至少四条、覆盖三个领域的经历。只使用日期和类型评分,不会从描述中猜测。
</p>
<div className="birth-time-event-list">
{drafts.map((draft, index) => (