feat(rectification): 少问几道就出卡,交付卡以范围为主(D1–D4)
- D1 引导窗口题每个校正最多 2 道(前端 GUIDED_WINDOW_CASE_LIMIT;引擎 GUIDED_COLLECT_LIMIT 未动:event_probes.py 属冻结评分身份,改它需重新冻结) - D2 七条定向线与跳过线重问问完即出卡,没问到的引导窗口不再挡卡,出卡后也不再挂窗口题 - D3 卡头加副标题「最可能 HH:MM」 - D4 前两列相差 ≥5 个百分点才显示相对可能性,否则一句「这几个时刻目前区分不开……」 - 离线回放 scripts/research/fewer_probes_card_replay.py:真值不降、宽度中位 ±1 分钟、提问 11.4→7.8 - Skill 10.0.29 → 10.0.30;DESIGN / VOICE / CHANGELOG / PROGRESS / 真机清单 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7ddce2c96a
commit
4e6e8d87ef
@@ -3516,6 +3516,20 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-range-delivery__accept:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
/* D3 (2026-09-26): representative minute as the subtitle under the range title. */
|
||||
.rectification-candidates-heading .rectification-range-delivery__most-likely {
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-md);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* D4 (2026-09-26): replaces the per-column numbers when the top two are < 5 points apart. */
|
||||
.rectification-range-delivery__indistinct {
|
||||
margin: 0;
|
||||
color: var(--color-ink);
|
||||
font-size: var(--type-body-md);
|
||||
line-height: 1.65;
|
||||
}
|
||||
.rectification-range-delivery__invite {
|
||||
margin: 0;
|
||||
color: var(--color-ink);
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
RECTIFICATION_USER_COPY,
|
||||
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
||||
rangeDeliveryEventCopy,
|
||||
rangeDeliveryMostLikely,
|
||||
rangeDeliveryShowsPercents,
|
||||
} from "@/lib/rectification-agentic/user-copy";
|
||||
import type { RangeDeliveryProjection } from "@/lib/rectification-agentic/v9/divergence-panel";
|
||||
import {
|
||||
@@ -51,11 +53,21 @@ export function RectificationRangeDelivery({
|
||||
// it actually does — swap the charting clock — instead of the neutral 「更像这个」.
|
||||
const recordConflict = delivery?.record_conflict ?? null;
|
||||
const caption = delivery?.narrow_hint ?? null;
|
||||
// D3: the range is the title; the representative minute is the subtitle.
|
||||
const mostLikely = delivery?.representative_time ?? result.representativeTime;
|
||||
// D4: per-column numbers only when the first column leads by ≥ 5 points.
|
||||
const showPercents = rangeDeliveryShowsPercents(columns);
|
||||
const indistinct = columns.length >= 2 && !showPercents;
|
||||
|
||||
return (
|
||||
<section className="rectification-candidates rectification-range-delivery" aria-label="生时校正区间">
|
||||
<div className="rectification-candidates-heading">
|
||||
<strong>{title}</strong>
|
||||
{mostLikely ? (
|
||||
<span className="rectification-range-delivery__most-likely">
|
||||
{rangeDeliveryMostLikely(mostLikely)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{caption ? (
|
||||
<p className="rectification-range-delivery__narrow">{caption}</p>
|
||||
@@ -71,6 +83,11 @@ export function RectificationRangeDelivery({
|
||||
{delivery?.tie_break_note ? (
|
||||
<p className="rectification-range-delivery__narrow">{delivery.tie_break_note}</p>
|
||||
) : null}
|
||||
{indistinct ? (
|
||||
<p className="rectification-range-delivery__indistinct">
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryIndistinct}
|
||||
</p>
|
||||
) : null}
|
||||
{sharedTraits.length > 0 ? (
|
||||
<ul className="rectification-range-delivery__shared">
|
||||
{sharedTraits.map((line) => (
|
||||
@@ -93,11 +110,13 @@ export function RectificationRangeDelivery({
|
||||
>
|
||||
<div className="rectification-range-delivery__time">{candidateDate ? `${candidateDate}${relativeDay} ${column.time}` : column.time}</div>
|
||||
{candidateDate ? <p className="rectification-range-delivery__muted">采用后,排盘使用这一日期和时间;原填报日期保留。</p> : null}
|
||||
<p className="rectification-range-delivery__likelihood">
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryRelativeLikelihood}
|
||||
{" "}
|
||||
{column.probability_percent}%
|
||||
</p>
|
||||
{showPercents ? (
|
||||
<p className="rectification-range-delivery__likelihood">
|
||||
{RECTIFICATION_USER_COPY.rangeDeliveryRelativeLikelihood}
|
||||
{" "}
|
||||
{column.probability_percent}%
|
||||
</p>
|
||||
) : null}
|
||||
{columnHasTraits(column) ? (
|
||||
<div className="rectification-range-delivery__block">
|
||||
{column.traits.d9 ? <p>{column.traits.d9}</p> : null}
|
||||
|
||||
@@ -239,7 +239,12 @@ export type DecideRectificationInput = Readonly<{
|
||||
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. */
|
||||
/**
|
||||
* The lines that may hold the card are asked out: skip-retry and the
|
||||
* targeted seven (production also requires the engine refresh). Since D2
|
||||
* (2026-09-26) unasked guided boundary windows are not part of this — see
|
||||
* `cardHoldingLinesExhausted`.
|
||||
*/
|
||||
guidedCollectExhausted?: boolean;
|
||||
/** Opening search window from `case.candidateRange`. Omit in helper/unit paths. */
|
||||
openingCandidateRange?: readonly [string, string] | null;
|
||||
@@ -660,8 +665,11 @@ function stillNeedNarrowing(input: DecideRectificationInput): boolean {
|
||||
/**
|
||||
* D2 + D6. The precision gate only holds the card back **while there is still
|
||||
* a question to ask**: delivery is `userStopped || everyLineAsked`, where
|
||||
* `everyLineAsked` is the guided pool (windows + skip retry + uncovered
|
||||
* domains), the engine refresh, and the targeted seven. Once all of them are
|
||||
* `everyLineAsked` is the skip retry, the engine refresh, and the targeted
|
||||
* seven. Product 2026-09-26 D2: guided boundary windows (at most two per
|
||||
* Case, `GUIDED_WINDOW_CASE_LIMIT`) are asked when they come up, but no longer hold the card — once
|
||||
* the targeted seven and their one re-ask are asked the card goes out with
|
||||
* the gate unmet. Once all of them are
|
||||
* asked out there is nothing left to collect, so the reader gets the card
|
||||
* under the existing rules whether or not D1 is met — D2 says the card, the
|
||||
* adopt button, and the three body lines do not change, and carry no
|
||||
|
||||
@@ -165,6 +165,7 @@ export const RECTIFICATION_USER_COPY = {
|
||||
rangeDeliveryTitle: "目前范围",
|
||||
rangeDeliveryMoreLikeThis: "更像这个",
|
||||
rangeDeliveryRelativeLikelihood: "相对可能性",
|
||||
rangeDeliveryIndistinct: "这几个时刻目前区分不开,补一件带年月的经历能帮助分开。",
|
||||
rangeDeliveryNoWindow: "未来一年没有明显的时段",
|
||||
rangeDeliveryNotCompared: "这一分钟还没对照",
|
||||
rangeDeliveryAdopting: "正在采用…",
|
||||
@@ -187,6 +188,28 @@ export function rangeDeliveryTopTwoTied(
|
||||
<= RANGE_DELIVERY_TIE_PERCENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* D4 (product 2026-09-26): per-column relative likelihood is shown only when
|
||||
* the first column leads the second by at least this many percentage points.
|
||||
* Below it the numbers carry no discrimination (the in-window constant part of
|
||||
* every event score is ~5x the part that moves with the minute), so the card
|
||||
* shows no numbers and one `rangeDeliveryIndistinct` sentence instead.
|
||||
*/
|
||||
export const RANGE_DELIVERY_PERCENT_MIN_GAP = 5;
|
||||
|
||||
export function rangeDeliveryShowsPercents(
|
||||
columns: readonly { probability_percent: number }[],
|
||||
): boolean {
|
||||
if (columns.length < 2) return false;
|
||||
const ranked = columns.map((column) => column.probability_percent).sort((left, right) => right - left);
|
||||
return ranked[0]! - ranked[1]! >= RANGE_DELIVERY_PERCENT_MIN_GAP;
|
||||
}
|
||||
|
||||
/** D3 (product 2026-09-26): subtitle under the range title. */
|
||||
export function rangeDeliveryMostLikely(time: string): string {
|
||||
return `最可能 ${time}`;
|
||||
}
|
||||
|
||||
export const RANGE_DELIVERY_DOMAIN_LABEL: Readonly<Record<string, string>> = {
|
||||
education: "学业",
|
||||
career: "事业",
|
||||
@@ -551,6 +574,8 @@ export function listUserVisibleCopy(): string[] {
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryTitle,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryMoreLikeThis,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryRelativeLikelihood,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryIndistinct,
|
||||
rangeDeliveryMostLikely("05:07"),
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryNoWindow,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryNotCompared,
|
||||
RECTIFICATION_USER_COPY.rangeDeliveryAdopting,
|
||||
|
||||
@@ -90,4 +90,5 @@ export const MAX_RESUMABLE_CASES_PER_USER = 1;
|
||||
|
||||
export const RECTIFICATION_SKILL_NAME = "jyotish-birth-time-rectification";
|
||||
// 原值: "10.0.28" / 新值: "10.0.29" / 原因: 年月阶段改口述并禁止追问本人
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.29";
|
||||
// 原值: "10.0.29" / 新值: "10.0.30" / 原因: D2 出卡句与 D4 卡上百分比规则写进 Skill(2026-09-26)
|
||||
export const RECTIFICATION_SKILL_VERSION = "10.0.30";
|
||||
|
||||
@@ -1033,6 +1033,30 @@ function windowAlreadyAsked(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* D1 (product 2026-09-26): at most two guided boundary windows per Case.
|
||||
* Enforced here, not by lowering the engine's `GUIDED_COLLECT_LIMIT` (6, in
|
||||
* `scripts/rectification/event_probes.py`): that file is part of the frozen
|
||||
* scoring identity (`scripts/research/sealed_holdout_rerun.py`
|
||||
* `PRODUCTION_FILES`), and any byte change there needs a research re-freeze
|
||||
* this change was not authorised for (BUG-1047 anti-recurrence). The receipt
|
||||
* keeps its ranked windows; the Case asks the first two it has not asked, and
|
||||
* a new receipt after an answer cannot add a third.
|
||||
*/
|
||||
export const GUIDED_WINDOW_CASE_LIMIT = 2;
|
||||
|
||||
function askedGuidedWindowCount(topics: readonly CollectionTopic[]): number {
|
||||
const asked = new Set<string>();
|
||||
for (const topic of topics) {
|
||||
const status = topicStatus(topic);
|
||||
if (status === "active" || !status) continue;
|
||||
const ref = parseGuidedWindowQuestionId(topicQuestionId(topic));
|
||||
if (!ref || ref.stage !== "existence") continue;
|
||||
asked.add(`${ref.year}:${ref.monthLo}:${ref.monthHi}`);
|
||||
}
|
||||
return asked.size;
|
||||
}
|
||||
|
||||
export function guidedWindowPool(
|
||||
windows: readonly GuidedCollectWindow[],
|
||||
evidence: readonly CollectionEvidence[],
|
||||
@@ -1043,6 +1067,7 @@ export function guidedWindowPool(
|
||||
): CollectionPoolItem[] {
|
||||
if (pendingTargetedYearDomain(declinedTopics, evidence)) return [];
|
||||
if (pendingGuidedYearWindow(declinedTopics, evidence)) return [];
|
||||
if (askedGuidedWindowCount(declinedTopics) >= GUIDED_WINDOW_CASE_LIMIT) return [];
|
||||
const declined = collectDeclinedKinds(declinedTopics);
|
||||
const covered = coveredCollectKinds(evidence);
|
||||
const openDomains = COLLECT_KIND_ORDER.filter((kind) => (
|
||||
@@ -1274,6 +1299,36 @@ export function guidedCollectExhausted(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* D2 (product 2026-09-26): the lines that may still hold the delivery card
|
||||
* are the targeted seven and their one re-ask (plus a year follow-up already
|
||||
* in progress). Guided boundary windows are still asked while they come up
|
||||
* first (at most `GUIDED_WINDOW_CASE_LIMIT` = 2 per Case), but an unasked
|
||||
* window no longer keeps the card back: offline replay shows they add
|
||||
* questions without moving the delivered range
|
||||
* (`docs/research/fewer_probes_card_replay_2026_09_26.json`).
|
||||
*/
|
||||
export function cardHoldingLinesExhausted(
|
||||
remainingLayers: readonly string[],
|
||||
evidence: readonly CollectionEvidence[],
|
||||
declinedTopics: readonly CollectionTopic[] = [],
|
||||
splitTimes?: readonly [string, string] | null,
|
||||
candidateCount?: number,
|
||||
): boolean {
|
||||
if (pendingTargetedYearDomain(declinedTopics, evidence)) return false;
|
||||
if (pendingGuidedYearWindow(declinedTopics, evidence)) return false;
|
||||
if (guidedRetryPool(evidence, declinedTopics, splitTimes, candidateCount).length > 0) {
|
||||
return false;
|
||||
}
|
||||
return targetedCollectExhausted(
|
||||
remainingLayers,
|
||||
evidence,
|
||||
declinedTopics,
|
||||
splitTimes,
|
||||
candidateCount,
|
||||
);
|
||||
}
|
||||
|
||||
export const COLLECT_FLOW_BANNED_PHRASES = [
|
||||
"任何领域",
|
||||
"领域不限",
|
||||
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
remainingSplitLayers,
|
||||
remainingSplitTimes,
|
||||
targetedCollectExhausted,
|
||||
guidedCollectExhausted,
|
||||
cardHoldingLinesExhausted,
|
||||
type GuidedCollectWindow,
|
||||
} from "./collection-question-pool.ts";
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
@@ -587,14 +587,15 @@ function narrowingExhaustion(
|
||||
live.remainingSplitTimes,
|
||||
live.remainingCandidateCount,
|
||||
),
|
||||
// F3/BUG-751: an empty guided pool is not "asked out" while the engine
|
||||
// refresh has never been tried (BUG-654 / BUG-656).
|
||||
// F3/BUG-751: nothing is "asked out" while the engine refresh has never
|
||||
// been tried (BUG-654 / BUG-656). D2 (2026-09-26): once the targeted seven
|
||||
// and their one re-ask are asked, the card is delivered — unasked guided
|
||||
// windows no longer hold it back for the precision gate.
|
||||
guidedCollectExhausted: options?.guidedCollectExhausted
|
||||
?? (refreshDone && guidedCollectExhausted(
|
||||
?? (refreshDone && cardHoldingLinesExhausted(
|
||||
remainingLayers,
|
||||
dossier.evidence,
|
||||
declined,
|
||||
live.guidedCollectWindows,
|
||||
live.remainingSplitTimes,
|
||||
live.remainingCandidateCount,
|
||||
)),
|
||||
|
||||
@@ -134,6 +134,7 @@ export { GENERIC_COLLECT_QUESTION };
|
||||
import {
|
||||
RECTIFICATION_TERMINATION_COPY,
|
||||
decideRectification,
|
||||
sessionOutcomeAllowsDelivery,
|
||||
type HoldoutValidationStatus,
|
||||
} from "../core/rectification-decision.ts";
|
||||
import type { ConflictProbe } from "../core/types.ts";
|
||||
@@ -3140,6 +3141,9 @@ export function buildMethodFollowupPlan(input: {
|
||||
&& datedPoolEmpty
|
||||
&& !input.tieBreakRequested
|
||||
) {
|
||||
// D2 (2026-09-26): once the card is out, unasked guided windows are not
|
||||
// asked any more (they no longer hold the card, so they must not ride
|
||||
// along under it either). Retry and targeted lines are unchanged.
|
||||
const targeted = targetedCollectFollowup(
|
||||
input.remainingLayers ?? [],
|
||||
input.evidence,
|
||||
@@ -3150,7 +3154,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
input.remainingSplitTimes,
|
||||
input.remainingCandidateCount,
|
||||
input.remainingCredibleRange,
|
||||
input.guidedWindows ?? [],
|
||||
sessionOutcomeAllowsDelivery(sessionOutcome) ? [] : input.guidedWindows ?? [],
|
||||
);
|
||||
if (targeted) next = targeted;
|
||||
else if (pendingHoldout) next = makeFollowup(pendingHoldout, false);
|
||||
|
||||
Reference in New Issue
Block a user