Files
Jyotisha/frontend/src/lib/rectification-agentic/core/distinguish-contract.ts
T
Jesse_ChenandCursor a0ce55f066
Independent Staging Quality Gate / validate (push) Successful in 11m17s
Independent Staging Quality Gate / publish (push) Failing after 9m28s
fix(web): drive rectification from candidate contrast and allow range close
Showing a choice card is no longer treated as completion. Distinguish probes
require real candidate groups, holdout stays out of scoring, and ordinary
sessions can finish with a credible range instead of an exact-minute gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 17:19:26 +08:00

80 lines
2.9 KiB
TypeScript

/**
* Candidate-discriminator contract shared by engine parsers, follow-ups, and CI.
*
* A distinguish probe must split at least two candidates into at least two
* expected outcomes with positive information gain. Question wording is not
* part of the contract.
*/
const CLOCK = /^(?:[01]?\d|2[0-3]):[0-5]\d$/;
export type DistinguishContractProbe = Readonly<{
role?: string | null;
phase?: string | null;
information_gain?: number;
informationGain?: number;
candidate_ids?: readonly unknown[] | null;
candidateIds?: readonly unknown[] | null;
expected_outcomes?: readonly unknown[] | null;
expectedOutcomes?: readonly unknown[] | null;
}>;
export function isDistinguishRole(probe: DistinguishContractProbe): boolean {
return probe.role === "distinguish" || probe.phase === "candidate_discriminator";
}
function asClock(value: unknown): string | null {
if (typeof value !== "string") return null;
const time = value.slice(0, 5);
return CLOCK.test(time) ? (time.length === 5 ? time : time.padStart(5, "0")) : null;
}
function outcomeRows(probe: DistinguishContractProbe): readonly Readonly<Record<string, unknown>>[] {
const rows = probe.expected_outcomes ?? probe.expectedOutcomes ?? [];
if (!Array.isArray(rows)) return [];
return rows.flatMap((row) => (
row && typeof row === "object" && !Array.isArray(row)
? [row as Readonly<Record<string, unknown>>]
: []
));
}
export function candidateIdsFromProbe(probe: DistinguishContractProbe): string[] {
const direct = probe.candidate_ids ?? probe.candidateIds ?? [];
const ids: string[] = [];
const seen = new Set<string>();
const push = (value: unknown) => {
const time = asClock(value);
if (!time || seen.has(time)) return;
seen.add(time);
ids.push(time);
};
if (Array.isArray(direct)) {
for (const item of direct) push(item);
}
for (const row of outcomeRows(probe)) {
for (const key of ["supports", "conflicts", "supportsCandidateIds", "conflictsCandidateIds"]) {
const values = row[key];
if (!Array.isArray(values)) continue;
for (const item of values) push(item);
}
}
return ids;
}
export function distinguishContractErrors(probe: DistinguishContractProbe): string[] {
if (!isDistinguishRole(probe)) return [];
const errors: string[] = [];
const outcomes = outcomeRows(probe);
if (outcomes.length < 2) errors.push("distinguish_empty_expected_outcomes");
if (candidateIdsFromProbe(probe).length < 2) errors.push("distinguish_empty_candidate_ids");
const gainRaw = "information_gain" in probe ? probe.information_gain : probe.informationGain;
const gain = typeof gainRaw === "number" && Number.isFinite(gainRaw) ? gainRaw : 0;
if (gain <= 0) errors.push("distinguish_non_positive_information_gain");
return errors;
}
export function isValidDistinguishProbe(probe: DistinguishContractProbe): boolean {
return isDistinguishRole(probe) && distinguishContractErrors(probe).length === 0;
}