fix(web): stop adopting tied rectification candidates after method coverage
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Coverage complete only unlocks discrimination. A 34/33/33 window plus an
occupation note must ask a D9/D10 contrast probe instead of offering a
stale winner card.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-24 12:19:36 +08:00
co-authored by Cursor
parent 3b785b821d
commit 3f1998bdb6
19 changed files with 1345 additions and 80 deletions
@@ -0,0 +1,182 @@
/**
* Chart differences that can actually eliminate candidates.
* Window-scan prose in the final report is not a substitute for this packet.
*/
export type ContrastExpectedOutcome = Readonly<{
outcomeId: string;
supportsCandidateIds: readonly string[];
conflictsCandidateIds: readonly string[];
}>;
export type ContrastSourceFeature = Readonly<{
technique: string;
calculationResultId: string | null;
}>;
export type CandidateDiscriminatorProbe = Readonly<{
probeId: string;
candidateSetVersion: string;
question: string;
expectedOutcomes: readonly ContrastExpectedOutcome[];
candidateSplitHash: string;
informationGain: number;
sourceFeatures: readonly ContrastSourceFeature[];
domain: string | null;
year: number | null;
semanticKey: string;
}>;
export type VargaDifference = Readonly<{
layer: string;
signs: readonly string[];
}>;
export type CandidateContrastPacket = Readonly<{
candidateSetVersion: string;
probes: readonly CandidateDiscriminatorProbe[];
vargaDifferences: readonly VargaDifference[];
}>;
export type EngineContrastProbe = Readonly<{
semantic_key?: string;
candidate_split_hash?: string;
domain?: string;
year?: number;
question?: string;
user_meaning?: string;
information_gain?: number;
expected_outcomes?: readonly Readonly<{
answer_class?: string;
supports?: readonly string[];
conflicts?: readonly string[];
}>[];
left_time?: string;
right_time?: string;
}>;
const D10_PREDICTIONS: Readonly<Record<string, string>> = {
: "某一阶段身份独立、承担主导职责",
: "稳定技术或事务执行、在组织内慢慢积累",
: "沟通、写作或频繁切换任务",
: "照顾、家庭或情感劳动更重的职责",
: "站到台前、带人、或公开担责",
: "稳定技术执行、组织内分析或服务",
: "跨领域合作、工作环境或合作模式变化",
: "研究、转化、或把一件事做深",
: "跨领域、教学或更开阔的工作环境",
: "管理职责、长期结构、延迟回报",
: "独立技术路线、非传统协作",
: "服务、艺术或界限更模糊的工作",
};
function signKey(value: string): string {
return value.replace(/座$/, "").trim();
}
export function buildCandidateContrastPacket(input: {
candidateSetVersion: string;
calculationResultId?: string | null;
engineProbes?: readonly EngineContrastProbe[];
vargaDifferences?: readonly VargaDifference[];
askedKeys?: readonly string[];
}): CandidateContrastPacket {
const asked = new Set(input.askedKeys ?? []);
const fromEngine = (input.engineProbes ?? []).flatMap((probe) => {
const built = probeFromEngine(probe, input.candidateSetVersion, input.calculationResultId ?? null);
if (!built) return [];
if (asked.has(built.semanticKey) || asked.has(built.candidateSplitHash) || asked.has(built.probeId)) {
return [];
}
return [built];
});
const vargaDifferences = input.vargaDifferences ?? [];
const fromVarga = vargaProbe(vargaDifferences, input.candidateSetVersion, input.calculationResultId ?? null, asked);
const probes = [...fromEngine, ...(fromVarga ? [fromVarga] : [])]
.sort((left, right) => right.informationGain - left.informationGain);
return {
candidateSetVersion: input.candidateSetVersion,
probes,
vargaDifferences,
};
}
export function selectDiscriminatorProbe(
packet: CandidateContrastPacket | null | undefined,
): CandidateDiscriminatorProbe | null {
const ranked = (packet?.probes ?? []).filter((probe) => probe.expectedOutcomes.length >= 2);
return ranked[0] ?? null;
}
function probeFromEngine(
probe: EngineContrastProbe,
candidateSetVersion: string,
calculationResultId: string | null,
): CandidateDiscriminatorProbe | null {
const outcomes = (probe.expected_outcomes ?? []).flatMap((row) => {
const outcomeId = typeof row.answer_class === "string" ? row.answer_class : "";
const supports = row.supports ?? [];
const conflicts = row.conflicts ?? [];
if (!outcomeId) return [];
return [{ outcomeId, supportsCandidateIds: supports, conflictsCandidateIds: conflicts }];
});
if (outcomes.length < 2 && probe.left_time && probe.right_time && probe.left_time !== probe.right_time) {
outcomes.push(
{ outcomeId: "yes", supportsCandidateIds: [probe.left_time], conflictsCandidateIds: [probe.right_time] },
{ outcomeId: "no", supportsCandidateIds: [probe.right_time], conflictsCandidateIds: [probe.left_time] },
);
}
if (outcomes.length < 2) return null;
const semanticKey = probe.semantic_key ?? `${probe.domain ?? "career"}.${probe.year ?? "contrast"}`;
const split = probe.candidate_split_hash ?? semanticKey;
const question = probe.question ?? probe.user_meaning ?? "";
if (!question.trim()) return null;
return {
probeId: `contrast:${semanticKey}:${split}`,
candidateSetVersion,
question,
expectedOutcomes: outcomes,
candidateSplitHash: split,
informationGain: probe.information_gain ?? 0,
sourceFeatures: [{ technique: probe.domain ?? "event_probe", calculationResultId }],
domain: probe.domain ?? null,
year: probe.year ?? null,
semanticKey,
};
}
function vargaProbe(
differences: readonly VargaDifference[],
candidateSetVersion: string,
calculationResultId: string | null,
asked: ReadonlySet<string>,
): CandidateDiscriminatorProbe | null {
const d10 = differences.find((item) => item.layer === "d10" && item.signs.length >= 2);
const d9 = differences.find((item) => item.layer === "d9" && item.signs.length >= 2);
const chosen = d10 ?? d9;
if (!chosen) return null;
const semanticKey = `varga.${chosen.layer}.${chosen.signs.join("|")}`;
if (asked.has(semanticKey)) return null;
const predictions = chosen.signs.map((sign) => D10_PREDICTIONS[signKey(sign)] ?? `${sign} 这一段更常见的前事`);
const outcomes: ContrastExpectedOutcome[] = chosen.signs.map((sign, index) => ({
outcomeId: `supports_${signKey(sign)}`,
supportsCandidateIds: [sign],
conflictsCandidateIds: chosen.signs.filter((_, other) => other !== index),
}));
const layerLabel = chosen.layer.toUpperCase();
const question = chosen.layer === "d10"
? `当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事:更接近${predictions.join(",还是")}`
: `当前几个候选在关系盘上还分得开。请核对一段还没用进评分的感情前事,用来对照 ${layerLabel} 差异。`;
return {
probeId: `contrast:${semanticKey}`,
candidateSetVersion,
question,
expectedOutcomes: outcomes,
candidateSplitHash: semanticKey,
informationGain: 0.12,
sourceFeatures: [{ technique: layerLabel, calculationResultId }],
domain: chosen.layer === "d10" ? "career" : "relationship",
year: null,
semanticKey,
};
}
@@ -0,0 +1,58 @@
/**
* Candidate separation is not event-fit and not method coverage.
* 34/33/33 is a tie. A 1-point engine lead is not a winner.
*/
export const MIN_SEPARATION_LEAD = 8;
export type SeparationStatus = "not_separated" | "weak_lead" | "separated";
export type CandidateScoreRow = Readonly<{
id?: string;
time: string;
score: number;
}>;
export type CandidateSeparation = Readonly<{
sufficient: boolean;
status: SeparationStatus;
lead: number;
topShare: number;
representativeTime: string | null;
credibleRange: readonly string[];
ranked: readonly CandidateScoreRow[];
}>;
export function evaluateCandidateSeparation(
candidates: readonly CandidateScoreRow[],
): CandidateSeparation {
const ranked = [...candidates]
.filter((item) => Number.isFinite(item.score))
.sort((left, right) => {
if (right.score !== left.score) return right.score - left.score;
return left.time.localeCompare(right.time);
});
const top = ranked[0] ?? null;
const runnerUp = ranked[1] ?? null;
const total = ranked.reduce((sum, item) => sum + Math.max(item.score, 0), 0);
const lead = top && runnerUp ? top.score - runnerUp.score : (top ? MIN_SEPARATION_LEAD : 0);
const topShare = top && total > 0 ? Math.max(top.score, 0) / total : 0;
const status: SeparationStatus = !top || ranked.length < 2 || lead < MIN_SEPARATION_LEAD
? "not_separated"
: lead >= 20
? "separated"
: "weak_lead";
const peak = top?.score ?? 0;
const credibleRange = ranked
.filter((item) => peak - item.score < MIN_SEPARATION_LEAD)
.map((item) => item.time);
return {
sufficient: status !== "not_separated",
status,
lead,
topShare,
representativeTime: top?.time ?? null,
credibleRange: credibleRange.length > 0 ? credibleRange : (top ? [top.time] : []),
ranked,
};
}
@@ -0,0 +1,89 @@
import {
evaluateCandidateSeparation,
type CandidateScoreRow,
type CandidateSeparation,
} from "./candidate-separation.ts";
import type { CandidateDiscriminatorProbe } from "./candidate-contrast-packet.ts";
export type RectificationNextActionType =
| "ask_fact_collection"
| "ask_candidate_discriminator"
| "ask_holdout_validation"
| "offer_provisional_range"
| "ready_to_adopt";
export type HoldoutValidationStatus = "not_started" | "passed" | "failed" | "unavailable";
export type DecideNextActionInput = Readonly<{
methodCoverageAll: boolean;
proposeAllowed: boolean;
confirmationAllowed?: boolean;
userStopped?: boolean;
selectionAllowed?: boolean;
snapshotCurrent?: boolean;
candidateScores: readonly CandidateScoreRow[];
discriminatorProbe?: CandidateDiscriminatorProbe | null;
holdoutValidation?: HoldoutValidationStatus;
}>;
export type RectificationNextAction = Readonly<{
type: RectificationNextActionType;
separation: CandidateSeparation;
probe: CandidateDiscriminatorProbe | null;
}>;
/**
* Coverage complete only unlocks discrimination. It never grants adoption.
*/
export function decideNextAction(input: DecideNextActionInput): RectificationNextAction {
const separation = evaluateCandidateSeparation(input.candidateScores);
const probe = input.discriminatorProbe ?? null;
const holdout = input.holdoutValidation ?? "unavailable";
const userStopped = input.userStopped === true;
if (input.confirmationAllowed === true) {
return { type: "ready_to_adopt", separation, probe: null };
}
if (userStopped && input.selectionAllowed === true) {
if (!separation.sufficient) {
return { type: "offer_provisional_range", separation, probe: null };
}
return { type: "ready_to_adopt", separation, probe: null };
}
if (!input.methodCoverageAll || input.snapshotCurrent === false) {
return { type: "ask_fact_collection", separation, probe: null };
}
if (!separation.sufficient) {
if (probe) {
return { type: "ask_candidate_discriminator", separation, probe };
}
return { type: "offer_provisional_range", separation, probe: null };
}
if (holdout === "not_started") {
return { type: "ask_holdout_validation", separation, probe: null };
}
if (holdout === "failed") {
if (probe) {
return { type: "ask_candidate_discriminator", separation, probe };
}
return { type: "offer_provisional_range", separation, probe: null };
}
if (input.proposeAllowed !== true) {
return { type: "ask_fact_collection", separation, probe: null };
}
return { type: "ready_to_adopt", separation, probe: null };
}
export function sessionKindFromNextAction(
type: RectificationNextActionType,
): "collect_evidence" | "discriminate_candidates" | "validate_holdout" | "provisional_range" | "adopt_representative" {
if (type === "ask_fact_collection") return "collect_evidence";
if (type === "ask_candidate_discriminator") return "discriminate_candidates";
if (type === "ask_holdout_validation") return "validate_holdout";
if (type === "offer_provisional_range") return "provisional_range";
return "adopt_representative";
}
export function offerSessionKinds(): readonly string[] {
return ["adopt_representative", "awaiting_confirmation", "provisional_range"];
}
@@ -10,3 +10,7 @@ export * from "./build-state.ts";
export * from "./probes-from-engine.ts";
export * from "./decision-fingerprint.ts";
export * from "./compose-receipt.ts";
export * from "./candidate-separation.ts";
export * from "./decide-next-action.ts";
export * from "./candidate-contrast-packet.ts";
export * from "./snapshot-source.ts";
@@ -0,0 +1,77 @@
/**
* Candidate cards are keyed only on scoreable facts + birth profile + inference.
* An occupation_note that does not score must not stale the snapshot.
*/
export type CandidateSnapshotSource = Readonly<{
birthProfileFingerprint: string;
scoreableEvidenceFingerprint: string;
inferenceRevision: number;
candidateSetVersion: string;
scoringPolicyVersion: string;
}>;
export type SnapshotStaleReason =
| "birth_profile_changed"
| "scoreable_evidence_changed"
| "candidate_set_superseded"
| "inference_revision_changed";
export const SNAPSHOT_STALE_COPY: Readonly<Record<SnapshotStaleReason, string>> = {
birth_profile_changed: "出生资料已变化,候选已失效",
scoreable_evidence_changed: "可评分证据已变化,请重新比较候选",
candidate_set_superseded: "已有更新的候选结果",
inference_revision_changed: "候选后验已更新,请使用当前结果",
};
export function classifySnapshotStaleReason(
snapshot: CandidateSnapshotSource | null | undefined,
current: CandidateSnapshotSource,
): SnapshotStaleReason | null {
if (!snapshot) return "candidate_set_superseded";
if (
snapshot.birthProfileFingerprint
&& current.birthProfileFingerprint
&& snapshot.birthProfileFingerprint !== current.birthProfileFingerprint
) {
return "birth_profile_changed";
}
if (snapshot.scoreableEvidenceFingerprint !== current.scoreableEvidenceFingerprint) {
return "scoreable_evidence_changed";
}
if (snapshot.candidateSetVersion !== current.candidateSetVersion) {
return "candidate_set_superseded";
}
if (snapshot.inferenceRevision !== current.inferenceRevision) {
return "inference_revision_changed";
}
return null;
}
/** Cards stay current when only non-scoreable notes (occupation_note) change. */
export function scoreableSnapshotIsCurrent(
snapshot: CandidateSnapshotSource | null | undefined,
current: CandidateSnapshotSource,
): boolean {
if (!snapshot) return false;
return snapshot.scoreableEvidenceFingerprint === current.scoreableEvidenceFingerprint
&& snapshot.candidateSetVersion === current.candidateSetVersion
&& snapshot.inferenceRevision === current.inferenceRevision;
}
export function snapshotIsCurrent(
snapshot: CandidateSnapshotSource | null | undefined,
current: CandidateSnapshotSource,
): boolean {
return classifySnapshotStaleReason(snapshot, current) === null;
}
export function candidateSnapshotSource(input: CandidateSnapshotSource): CandidateSnapshotSource {
return {
birthProfileFingerprint: input.birthProfileFingerprint,
scoreableEvidenceFingerprint: input.scoreableEvidenceFingerprint,
inferenceRevision: input.inferenceRevision,
candidateSetVersion: input.candidateSetVersion,
scoringPolicyVersion: input.scoringPolicyVersion,
};
}