feat(rectification): 出卡加精度门槛,补经历改成系统点名
Independent Staging Quality Gate / validate (push) Failing after 6m28s
Independent Staging Quality Gate / publish (push) Skipped

宽度超过 10 分钟或头名并列时不再出交付卡,改为按大运边界逐条问、
用类型芯片和年/月选择器录入。跳过的线换问法再问一次;答「这类事
都没有过」的不再问。用户说「没有了」仍立刻给目前范围。Skill 10.0.27。

BUG-740~743
This commit is contained in:
jesse-ux
2026-09-16 18:35:27 +08:00
parent 317e9f1886
commit cfb41daf3d
75 changed files with 4763 additions and 383 deletions
@@ -0,0 +1,33 @@
/**
* Range-delivery precision gate (D1). Width, display-gap, and exact score
* tie are independent of scoring / cluster merge.
*/
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
import { RANGE_DELIVERY_TIE_PERCENT, rangeWidthMinutes } from "../user-copy.ts";
export function deliveryWidthMinutes(
range: readonly [string, string] | null | undefined,
): number | null {
if (!range?.[0] || !range[1]) return null;
if (range[0] === range[1]) return 0;
return rangeWidthMinutes(range[0], range[1]);
}
export function probabilityToPercent(value: number | null | undefined): number {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return 0;
const raw = value <= 1 ? value * 100 : value;
return Math.min(100, Math.round(raw));
}
export function computePrecisionGateMet(input: {
range: readonly [string, string] | null | undefined;
topTwoPercents: readonly number[];
tiedForFirst: boolean;
}): boolean {
if (input.tiedForFirst) return false;
const width = deliveryWidthMinutes(input.range);
if (width == null || width > RECTIFICATION_POLICY.deliveryMaxWidthMinutes) return false;
if (input.topTwoPercents.length < 2) return input.topTwoPercents.length === 1;
return input.topTwoPercents[0]! - input.topTwoPercents[1]! > RANGE_DELIVERY_TIE_PERCENT;
}
@@ -231,6 +231,10 @@ export type DecideRectificationInput = Readonly<{
windowWidenSuggested?: boolean;
refreshExhausted?: boolean;
targetedCollectExhausted?: boolean;
/** D1: width ≤ 10, top-two display gap > 3, not an exact first-place tie. */
precisionGateMet?: boolean;
/** Guided window / skip-retry / uncovered-domain pool is empty. */
guidedCollectExhausted?: boolean;
/** Opening search window from `case.candidateRange`. Omit in helper/unit paths. */
openingCandidateRange?: readonly [string, string] | null;
/** Unasked D9/D10 style questions remain. */
@@ -359,6 +363,16 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
&& !probe
&& input.targetedCollectExhausted !== false
) {
if (!mayDeliverOnPrecision(input)) {
return collect(
separation,
holdout,
range,
probe,
waitToNarrowCapability(rangeDeliveryCapability),
stopClass.reason,
);
}
return deliverRange(
input,
separation,
@@ -372,7 +386,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
if (coverageBlocks) {
const engineOffers = input.engineCeiling.acceptanceAllowed
|| input.engineCeiling.proposeAllowed;
const narrowingOpen = stillNeedNarrowing(input);
const narrowingOpen = stillNeedNarrowing(input) && !mayDeliverOnPrecision(input);
if (
stopClass?.kind !== "keep_collecting"
&& input.trainingGateOpen !== false
@@ -406,17 +420,37 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
return holdoutValidation(separation, range, capability);
}
// coverageBlocks already collected when the training gate is closed.
// Dated-pool empty is not delivery until refresh and targeted collect are
// exhausted (BUG-654). Personality still does not occupy this slot.
// Uncovered collect lines keep collecting only while D1 is unmet
// (BUG-654 + precision gate). Personality still does not occupy this slot.
// Omitted flags mean the helper/unit path: do not wait. Production
// decideFromDossier always passes explicit booleans.
if (stillNeedNarrowing(input)) {
if (stillNeedNarrowing(input) && !mayDeliverOnPrecision(input)) {
return collect(separation, holdout, range, probe, waitToNarrowCapability(capability), stopReason);
}
if (stopClass?.kind === "exhausted") {
if (!mayDeliverOnPrecision(input)) {
return collect(
separation,
holdout,
range,
probe,
waitToNarrowCapability(rangeDeliveryCapability),
stopReason,
);
}
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
}
if (rangeDeliveryCapability.canAdopt && input.methodCoverageAll) {
if (!mayDeliverOnPrecision(input)) {
return collect(
separation,
holdout,
range,
probe,
waitToNarrowCapability(rangeDeliveryCapability),
"probe_pool_exhausted",
);
}
return finish("adopt_representative", {
input,
separation,
@@ -427,9 +461,29 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
stopReason: "probe_pool_exhausted",
});
}
if (!mayDeliverOnPrecision(input)) {
return collect(
separation,
holdout,
range,
probe,
waitToNarrowCapability(rangeDeliveryCapability),
stopReason,
);
}
return deliverRange(input, separation, holdout, range, "offer", rangeDeliveryCapability);
}
if (stopClass?.kind === "exhausted") {
if (!mayDeliverOnPrecision(input)) {
return collect(
separation,
holdout,
range,
probe,
waitToNarrowCapability(rangeDeliveryCapability),
stopReason,
);
}
return deliverRange(input, separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
}
if (input.accepted) {
@@ -445,7 +499,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
capability: rangeDeliveryCapability,
});
}
if (input.datedMethodCollectOpen === true && !input.userStopped) {
if (input.datedMethodCollectOpen === true && !input.userStopped && !mayDeliverOnPrecision(input)) {
return collect(separation, holdout, range, null, capability, stopReason);
}
if (confirmationAllowed) {
@@ -588,6 +642,20 @@ function stillNeedNarrowing(input: DecideRectificationInput): boolean {
return input.refreshExhausted === false || input.targetedCollectExhausted === false;
}
/**
* Exhausted / offer delivery is blocked until D1, the user stops, or the
* guided pool is empty. Omitted flags keep the helper/unit path delivering.
*/
export function mayDeliverOnPrecision(input: DecideRectificationInput): boolean {
if (input.userStopped === true) return true;
if (input.guidedCollectExhausted === true) return true;
if (input.precisionGateMet === true) return true;
if (input.precisionGateMet === undefined && input.guidedCollectExhausted === undefined) {
return true;
}
return false;
}
export function shouldHoldForTieBreak(
input: Pick<
DecideRectificationInput,