diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 6d552e5a..1191a78b 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -6429,6 +6429,22 @@ - 复发自:BUG-403(候选卡、代表时间、可信区间必须共享 inference 投影,但读写口径未钉成同一函数) - 修复版本:待发布 +## BUG-424 | quarter/range 精度训练门计入、推断账本却标 unused + +- 状态:resolved +- 首次发现:2026-08-28 +- 最近更新:2026-08-28 +- 影响面:`datedPrecision`、`buildCaseInferenceState` 的 `asPrecision` +- 用户现象:账本允许 `quarter` / `range` 精度。训练门把它们当成带日期事件开门,推断状态却把同一批事件标成 `unknown`/`unused`,既不打分也不进 holdout。 +- 触发条件:录证据工具写入 `datePrecision: "quarter"` 或 `"range"` 后重建 `inference_state`。 +- 根因:`evidence-model.datedPrecision` 把 `quarter`/`range` 映射成 `year`,`inference-adapter.asPrecision` 映射成 `unknown`。 +- 修复:导出同一个 `datedPrecision`,推断适配器也走它。空值仍是 `unknown`。 +- 验证:`rectification-v9-contracts` 锁定 `quarter`/`range` → `year`。`rectification-credible-range-projection` 锁定 `quarter` 事件 `usage !== "unused"`。 +- 防复发:训练门与推断账本不得对同一 `datePrecision` 使用两套映射。 +- 相关记录:BUG-423 +- 复发自:无 +- 修复版本:待发布 + ## BUG-410 | 训练已齐仍因家人/职业方法层停在采集,Agent 只确认后截断 - 状态:resolved diff --git a/frontend/src/lib/rectification-agentic/v9/evidence-model.ts b/frontend/src/lib/rectification-agentic/v9/evidence-model.ts index da46e488..05035212 100644 --- a/frontend/src/lib/rectification-agentic/v9/evidence-model.ts +++ b/frontend/src/lib/rectification-agentic/v9/evidence-model.ts @@ -165,7 +165,7 @@ export function datePrecisionRank(precision: string): number { return PRECISION_RANK[precision] ?? -1; } -function datedPrecision(value: string): DatedEventInput["precision"] { +export function datedPrecision(value: string): DatedEventInput["precision"] { if (value === "day" || value === "month" || value === "year" || value === "unknown") return value; return "year"; } diff --git a/frontend/src/lib/rectification-agentic/v9/inference-adapter.ts b/frontend/src/lib/rectification-agentic/v9/inference-adapter.ts index bd222bd3..13767004 100644 --- a/frontend/src/lib/rectification-agentic/v9/inference-adapter.ts +++ b/frontend/src/lib/rectification-agentic/v9/inference-adapter.ts @@ -15,6 +15,7 @@ import { rankActive } from "../core/convergence-evaluator.ts"; import { rangeFromTimes, unionStillValidRange } from "../core/credible-range.ts"; import { previousInferenceFromReceipt } from "../core/compose-receipt.ts"; import type { AnswerClass, ConflictProbe, InferenceState } from "../core/types.ts"; +import { datedPrecision } from "./evidence-model.ts"; import { isHoldoutVerificationQuote, type ChoiceKey, @@ -28,8 +29,8 @@ function yearFrom(value: string | null | undefined): number | null { } function asPrecision(value: string | null | undefined): EngineEventInput["precision"] { - if (value === "day" || value === "month" || value === "year") return value; - return "unknown"; + if (!value) return "unknown"; + return datedPrecision(value); } export function askedProbeKeysFromReceipt( diff --git a/frontend/tests/rectification-credible-range-projection.test.ts b/frontend/tests/rectification-credible-range-projection.test.ts index 7ab4b8c5..dbf59e2d 100644 --- a/frontend/tests/rectification-credible-range-projection.test.ts +++ b/frontend/tests/rectification-credible-range-projection.test.ts @@ -196,6 +196,24 @@ test("a corrupted receipt range outside the window still fail-closes", () => { assert.equal(projection.credibleRange, null); }); +test("quarter-precision evidence stays dated in the inference ledger", () => { + const state = buildCaseInferenceState({ + range: { start_time: "05:00", end_time: "05:10" }, + candidates: [ + { candidateId: "05:00", time: "05:00", relativeSupport: 50 }, + { candidateId: "05:10", time: "05:10", relativeSupport: 50 }, + ], + evidence: [ + { id: "q-career", domain: "career", occurredFrom: "2018-04-01", datePrecision: "quarter" }, + { id: "y-edu", domain: "education", occurredFrom: "2016-01-01", datePrecision: "year" }, + ], + probes: [], + }); + const quarter = state.events.find((item) => item.id === "q-career"); + assert.equal(quarter?.precision, "year"); + assert.notEqual(quarter?.usage, "unused"); +}); + test("buildCaseInferenceState receipt is the producer of the projected range", () => { const state = buildCaseInferenceState({ range: { start_time: "04:55", end_time: "05:02" }, diff --git a/frontend/tests/rectification-v9-contracts.test.ts b/frontend/tests/rectification-v9-contracts.test.ts index 7e29a259..2dbe4f8a 100644 --- a/frontend/tests/rectification-v9-contracts.test.ts +++ b/frontend/tests/rectification-v9-contracts.test.ts @@ -29,6 +29,7 @@ import { isEvidenceDomain, isEvidenceKind, isEvidenceStatus, + datedPrecision, normalizeQuote, quoteIsGroundedInMessage, } from "../src/lib/rectification-agentic/v9/evidence-model.ts"; @@ -130,6 +131,10 @@ test("evidence model exposes the full kind/domain/precision/status sets", () => assert.equal(isDatePrecision(precision), true); } assert.equal(isDatePrecision("exact_minute"), false); + assert.equal(datedPrecision("quarter"), "year"); + assert.equal(datedPrecision("range"), "year"); + assert.equal(datedPrecision("day"), "day"); + assert.equal(datedPrecision("unknown"), "unknown"); for (const status of ["draft", "pending_confirmation", "confirmed", "superseded", "rejected"]) { assert.equal(isEvidenceStatus(status), true); }