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
@@ -312,6 +312,17 @@ const careerEvidence = {
created_at: "2026-08-12T10:00:09.000Z",
};
// R1(BUG-1193):第 4 件带日期经历(与 careerEvidence 同领域),让原 3 件 3 域的场景跨过 4 件 3 域门槛。
const careerChangeEvidence = {
...careerEvidence,
id: "44444444-4444-4444-8444-4444444444c2",
event_kind: "career_change",
occurred_from: "2022-03-01",
date_precision: "month",
summary: "2022年3月换工作",
created_at: "2026-08-12T10:00:10.000Z",
};
const methodCoverageTieEvidence = [
{ ...educationEvidence, domain: "education" },
{
@@ -899,7 +910,8 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => 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 },