fix(rectification): feed cluster span into confirmation-gate refresh paths
Independent Staging Quality Gate / validate (push) Failing after 9m55s
Independent Staging Quality Gate / publish (push) Has been skipped

Representative minutes still undercounted width on dossier refresh and parsed snapshots even after the engine used cluster coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-10 17:51:07 +08:00
co-authored by Cursor
parent a998b6ec53
commit a1db6fe550
7 changed files with 69 additions and 5 deletions
+2 -2
View File
@@ -9934,8 +9934,8 @@
- 用户现象:报告宽度跟区间两端对得上,引擎「不可分宽度」却按各簇代表分钟算,少算两端延伸;极端时可把 ≤5 分钟的确认门误开。
- 触发条件:各簇 `cluster_start` / `cluster_end` 超出代表分钟。
- 根因:宽度公式用代表分钟的 `max-min+1`,不是簇覆盖。
- 修复:改为 `max(cluster_end)-min(cluster_start)+1`;缺簇字段时回退到 `time``tied_minute_count` 不动。
- 验证:`tests/test_rectification_confirmation_and.py``frontend/tests/rectification-delivery-report-facts.test.ts``rectification-confirmation-gate.test.ts`
- 修复:改为 `max(cluster_end)-min(cluster_start)+1`;缺簇字段时回退到 `time``tied_minute_count` 不动。决策层与客户端解析把 `clusterStart` / `clusterEnd` 传进确认门。
- 验证:`tests/test_rectification_confirmation_and.py``frontend/tests/rectification-delivery-report-facts.test.ts``rectification-confirmation-gate.test.ts``rectification-candidate-result.test.ts`
- 防复发:两簇 04:5004:55 与 04:5605:02 必须得到 13,不得算成 7。确认门不得因代表分钟看起来相邻而打开。
- 相关记录:BUG-593
- 复发自:无
@@ -19,7 +19,7 @@ Python 按 **argmax 映射到所在簇** 比较,不改成代表分钟列表。
- **BUG-637**:账本派生的精确 `domain.year` 键只拦存在性探针。`known_event_quality` / `event_quality``askedKeysForSameYearDedup` 去掉这些键后再做同年去重;已答质量键仍丢。`remainingConflictProbes` 本来就跳过质量题,未改。不改 `SCORE_DELTA`、四选项、确认门。
- **BUG-638**`dasha_agreement(..., clusters=)` 与 TS `dashaAgreementAmongActive(..., clusters)` 按簇比较。`decision_policy``candidate_decisions` 传入 `build_refinement_packet`。引擎原值仍是 `dasha_agreement_pre_inference`。BUG-593 记录已补正。
- **BUG-639**`indistinguishable_width_minutes` / `indistinguishableWidthMinutes``max(cluster_end)-min(cluster_start)+1`;缺字段回退到 `time``tied_minute_count` 不动。
- **BUG-639**`indistinguishable_width_minutes` / `indistinguishableWidthMinutes``max(cluster_end)-min(cluster_start)+1`;缺字段回退到 `time``tied_minute_count` 不动。`decideFromDossier``parseRectificationCandidateResult``clusterStart` / `clusterEnd` 传进确认门,避免刷新路径只按代表分钟少算。
- **BUG-640**:推断代表分钟 ≠ 引擎代表分钟时,公开 `house_table` / `natal_recast` 跟推断分钟;引擎原值改名 `*_pre_inference`。Agent 可见投影剥掉这三个字段。不改引擎。
- 记录:BUG-389/593/637640、CHANGELOG、本文件。Skill 版本不变(`10.0.21`)。
@@ -96,6 +96,10 @@ export type DecisionDossier = Readonly<{
relativeSupport?: number;
posterior_score?: number;
tiedMinuteCount?: number;
clusterStart?: string;
clusterEnd?: string;
cluster_start?: string;
cluster_end?: string;
}>[];
representativeTime?: string | null;
evidenceLedgerFingerprint?: string | null;
@@ -667,6 +671,12 @@ export function decideFromDossier(
time: candidate.time,
rank: candidate.rank ?? Number.MAX_SAFE_INTEGER,
tiedMinuteCount: candidate.tiedMinuteCount ?? Number.MAX_SAFE_INTEGER,
...(candidate.clusterStart ?? candidate.cluster_start
? { clusterStart: candidate.clusterStart ?? candidate.cluster_start }
: {}),
...(candidate.clusterEnd ?? candidate.cluster_end
? { clusterEnd: candidate.clusterEnd ?? candidate.cluster_end }
: {}),
})),
decisionReceipt: latest?.decisionReceipt ?? null,
});
@@ -115,7 +115,6 @@ import {
isEventQualityProbe,
sameYearProbeAsked,
SEMANTIC_YEAR_KEY,
EXISTENCE_STYLE_OPTIONS,
type DroppedProbe,
type ProbeStyleOption,
} from "./probe-question-contract.ts";
@@ -44,6 +44,8 @@ export type RectificationCandidate = Readonly<{
time: string;
relativeSupport: number;
tiedMinuteCount: number;
clusterStart?: string;
clusterEnd?: string;
}>;
export type RectificationHouseRow = Readonly<{
@@ -329,7 +331,17 @@ export function parseRectificationCandidateResult(value: unknown): Rectification
|| relativeSupport === null || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100
|| tiedMinuteCount === null || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1
) return null;
candidates.push({ candidateId, rank, time: candidateTime, relativeSupport, tiedMinuteCount });
const clusterStart = time(candidate.clusterStart ?? candidate.cluster_start);
const clusterEnd = time(candidate.clusterEnd ?? candidate.cluster_end);
candidates.push({
candidateId,
rank,
time: candidateTime,
relativeSupport,
tiedMinuteCount,
...(clusterStart ? { clusterStart } : {}),
...(clusterEnd ? { clusterEnd } : {}),
});
}
const receipt = receiptFromSnapshot(snapshot);
@@ -343,4 +343,38 @@ test("inference marks are empty when inference_state is absent", () => {
assert.deepEqual(result.inferenceMarks, []);
});
test("parsed candidates keep cluster span so confirmation width is not undercounted", () => {
const result = parseRectificationCandidateResult({
...camelCaseSnapshot,
candidates: [
{
candidateId: CANDIDATE_ID,
rank: 1,
time: "04:53",
relativeSupport: 40,
tiedMinuteCount: 1,
clusterStart: "04:50",
clusterEnd: "04:55",
},
{
candidateId: SECOND_CANDIDATE_ID,
rank: 2,
time: "04:59",
relativeSupport: 39,
tiedMinuteCount: 1,
clusterStart: "04:56",
clusterEnd: "05:02",
},
],
});
assert.ok(result);
assert.equal(result.candidates[0]?.clusterStart, "04:50");
assert.equal(result.candidates[0]?.clusterEnd, "04:55");
const adjacent = result.confirmationGate.blockers.find(
(row) => row.id === "adjacent_minutes_indistinguishable",
);
assert.equal(adjacent?.indistinguishable_width_minutes, 13);
assert.equal(result.confirmationGate.confirmation_allowed, false);
});
@@ -414,6 +414,15 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
assert.equal(safeToolErrorCode(new RectificationToolServiceError("confirmation_blocked")), "confirmation_blocked");
});
test("decideFromDossier passes cluster span into the confirmation gate", () => {
const src = readFileSync(
new URL("../src/lib/rectification-agentic/v9/decision-from-dossier.ts", import.meta.url),
"utf8",
);
assert.match(src, /clusterStart: candidate\.clusterStart \?\? candidate\.cluster_start/);
assert.match(src, /clusterEnd: candidate\.clusterEnd \?\? candidate\.cluster_end/);
});
test("confirmation width uses cluster span so representative minutes cannot open the gate", () => {
const candidates = [
{ time: "04:53", rank: 1, tiedMinuteCount: 1, clusterStart: "04:50", clusterEnd: "04:55" },