fix(rectification): feed cluster span into confirmation-gate refresh paths
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:
+2
-2
@@ -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:50–04:55 与 04:56–05: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/637~640、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" },
|
||||
|
||||
Reference in New Issue
Block a user