fix(rectification): reject synthetic stale probes
Independent Staging Quality Gate / validate (push) Successful in 13m3s
Independent Staging Quality Gate / publish (push) Successful in 16m3s

This commit is contained in:
Jesse_Chen
2026-08-27 00:28:14 +08:00
parent 7d6b88c886
commit 1c0ee8db13
9 changed files with 166 additions and 87 deletions
@@ -42,7 +42,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
aria-label={props.card.scoring ? "校正判断" : "盘外核对"}
>
<fieldset className="birth-time-choice-question" disabled={props.pending || props.disabled || Boolean(selectedKey)}>
<legend>{props.card.prompt}</legend>
<legend className="sr-only">{props.card.prompt}</legend>
{props.card.why ? <p className="rectification-choice-why">{props.card.why}</p> : null}
<div className="birth-time-primary-choices">
{primary.map((option, index) => (
@@ -255,6 +255,13 @@ export function stampChoiceSchemaWithProbe(
probe_id: preferredId,
})) ?? null;
if (preferredKey && !matched) {
if (state) {
const unstamped = { ...schema };
delete unstamped.probe_id;
delete unstamped.semantic_key;
delete unstamped.candidate_split_hash;
return { ...unstamped, scoring };
}
return {
...schema,
probe_id: preferredId ?? `probe:${preferredKey}`,
@@ -337,49 +337,6 @@ function remainingReverseVerifyProbes(
return [...dasha, ...fallback].slice(0, MAX_REVERSE_VERIFY);
}
const QUALITY_ENCODED_RE = /失利|失常|复读|没考好|考砸|发挥不好|发挥失常|发挥异常|压力很大/;
function qualityAlreadyEncoded(
evidence: readonly MethodFollowupEvidence[],
domain: string,
year: number,
): boolean {
const nearby = existenceNearbyYears(domain);
return evidence.some((item) => {
if (item.status !== "confirmed" && item.status !== "draft" && item.status !== "pending_confirmation") {
return false;
}
if (item.domain !== domain) return false;
const itemYear = evidenceYear(item);
if (itemYear === null) return false;
if (Math.abs(itemYear - year) > nearby) return false;
return QUALITY_ENCODED_RE.test(item.summary ?? "");
});
}
function remainingQualityProbes(
probes: readonly DiscriminatingEventProbe[] | undefined,
evidence: readonly MethodFollowupEvidence[],
declined: ReadonlySet<string>,
askedKeys: ReadonlySet<string> = new Set(),
): DiscriminatingEventProbe[] {
const rows: DiscriminatingEventProbe[] = [];
for (const probe of probes ?? []) {
if (probe.source !== "known_event_quality") continue;
if (probe.domain !== "education") continue;
if (declined.has(probe.domain)) continue;
if (!probeYearAlreadyCovered(evidence, probe.domain, probe.year)) continue;
if (qualityAlreadyEncoded(evidence, probe.domain, probe.year)) continue;
const semantic = probe.semantic_key ?? `${probe.domain}.${probe.year}`;
const split = probe.candidate_split_hash ?? "";
if (askedKeys.has(semantic) || (split && askedKeys.has(split))) continue;
rows.push(probe);
}
return rows
.sort((left, right) => (right.information_gain ?? 0) - (left.information_gain ?? 0))
.slice(0, MAX_REVERSE_VERIFY);
}
function remainingConflictProbes(
probes: readonly DiscriminatingEventProbe[] | undefined,
evidence: readonly MethodFollowupEvidence[],
@@ -767,7 +724,9 @@ export function buildMethodFollowupPlan(input: {
const sessionOutcome = input.sessionOutcome ?? "collect_evidence";
const candidatesSeparated = input.candidatesSeparated === true;
const contrastProbe = selectDiscriminatorProbe(input.contrastPacket ?? null);
const focus = input.activeFocus ?? null;
// Legacy known-event quality cards were never backed by an inference probe.
// Ignore them so existing cases resume evidence collection instead of exposing a stale card.
const focus = input.activeFocus?.intent === "clarify_event" ? null : input.activeFocus ?? null;
const keepAcceptedFocus = Boolean(
focus && (focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check"),
);
@@ -907,14 +866,6 @@ export function buildMethodFollowupPlan(input: {
...(input.askedProbeKeys ?? []),
...askedKeysFromLedgerEvidence(input.evidence),
]);
const qualityProbe = dashaCovered
? remainingQualityProbes(
input.eventClarificationProbes ?? input.eventProbes,
input.evidence,
declined,
askedKeys,
)[0] ?? null
: null;
const conflictProbe = dashaCovered && meetsAcceptanceEventQuality(input.evidence)
? remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys)[0] ?? null
: null;
@@ -932,25 +883,6 @@ export function buildMethodFollowupPlan(input: {
),
source: "method_coverage",
});
} else if (qualityProbe) {
next = makeFollowup({
method_id: PROBE_METHOD_ID[qualityProbe.domain],
intent: "clarify_event",
ask_theme: REVERSE_VERIFY_THEME[qualityProbe.domain],
domain: qualityProbe.domain,
kind_hint: REVERSE_VERIFY_KIND[qualityProbe.domain],
user_prompt_hint: ask(
`已记下 ${qualityProbe.year_label} 的经历。按 choice_frame 问那次是否${qualityProbe.event_family}。对得上写入账本并重算;对不上关闭该问。不要发明年份。`,
REVERSE_VERIFY_VARGA[qualityProbe.domain],
),
source: "event_probe",
information_gain: qualityProbe.information_gain ?? 0,
semantic_key: qualityProbe.semantic_key ?? `${qualityProbe.domain}.${qualityProbe.year}`,
candidate_split_hash: qualityProbe.candidate_split_hash,
probe_year: qualityProbe.year,
choice_kind: qualityProbe.choice_kind ?? "event_quality",
style_options: qualityProbe.style_options,
}, true, true);
} else if (conflictProbe && (!coverageComplete || !candidatesSeparated || (conflictProbe.information_gain ?? 0) >= 0.08)) {
next = makeFollowup({
method_id: PROBE_METHOD_ID[conflictProbe.domain],
@@ -47,14 +47,12 @@ function schemaProbeId(schema: Readonly<Record<string, unknown>> | null | undefi
export function shouldSkipDiscriminatorFollowup(followup: MethodFollowup): PersistServerFocusStatus | null {
if (
followup.source === "event_probe"
&& followup.choice_kind !== "event_quality"
&& (followup.information_gain ?? 0) <= 0
) {
return "zero_information_gain";
}
if (
followup.intent === "distinguish_candidates"
&& followup.choice_kind !== "event_quality"
&& ((followup.candidate_ids?.length ?? 0) < 2 || (followup.expected_outcomes?.length ?? 0) < 2)
) {
return "zero_information_gain";
@@ -82,15 +80,17 @@ function expectedAnswerSchemaFor(
candidate_split_hash: followup.candidate_split_hash ?? null,
choice_kind: frame.choice_kind ?? followup.choice_kind ?? "existence",
};
return stampChoiceSchemaWithProbe(
const state = previousInferenceFromReceipt(decisionReceipt ?? null);
const stamped = stampChoiceSchemaWithProbe(
schema,
previousInferenceFromReceipt(decisionReceipt ?? null),
state,
questionId,
{
semantic_key: followup.semantic_key,
candidate_split_hash: followup.candidate_split_hash,
},
);
return state && stamped.scoring !== false && !schemaProbeId(stamped) ? null : stamped;
}
export function openQuestionFromPersistedFocus(result: PersistServerFocusResult): {