Files
Jyotisha/frontend/src/lib/rectification-candidate-result.ts
T
Jesse_ChenandClaude Opus 5.5 3733b9787b fix(rectification): bump scoring identity to scoring-10 for functional profile v2; dated contract by generation (BUG-1181)
Functional roles feed the *_functional_*_auxiliary rules, so 57782aea changes
candidate scores for identical input (memoization fixture 12:00: 8.6274 ->
8.4977). Per the "scoring semantics change => bump ALGORITHM_VERSION"
precedent (scoring-7 -> 8 -> 9), the identity moves to scoring-10; policy v3,
input contract v5 and Skill versions are unchanged, history is not relabeled.

Five frontend sites and one SQL guard tested `=== "...scoring-9"` for the
dated candidate-window contract; they now use isDatedScoringAlgorithmVersion /
a generation regex (>= 9). Migration 20261002010000 only recreates
validate_dated_rectification_candidate (one-line guard change).

Memoization golden v2 written by the test's own write_golden; v1 (scoring-8)
frozen by sha256. Real-engine scoring-10 cross-midnight golden added. Research
records re-frozen per ERR-110 (label functional_v2_2026_10_02) and
scripts/functional_benefics.py added to the frozen production identity
(ERR-114: 57782aea changed scores without tripping it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-02 12:31:50 +08:00

504 lines
21 KiB
TypeScript

import { isDatedScoringAlgorithmVersion, readCandidatePosition, parseWindowIntervals, type CandidatePosition, type WindowInterval } from "./rectification-agentic/core/candidate-window.ts";
import type {
DashaAgreement,
EventDashaLedgerRow,
LagnaContrast,
NakshatraBoundary,
OosBlindPrompt,
PrecisionStage,
WindowScanTransition,
} from "./rectification-agentic/v9/refinement-packet";
import {
parseWindowScanTransitions,
refinementFromDecisionReceipt,
} from "./rectification-agentic/v9/refinement-packet";
import { parseWindowScan } from "./rectification-agentic/v9/varga-observations";
import type { TechniqueAuditRow } from "./consultation-agent-events";
import { normalizeTechniqueAuditRows } from "./consultation-technique-audit";
import {
buildConfirmationGate,
type ConfirmationGate,
} from "./rectification-agentic/v9/confirmation-gate";
import { MIN_SEPARATION_LEAD } from "./rectification-agentic/core/candidate-separation";
import {
engineCapabilityCeilingFromReceipt,
sessionOutcomeAllowsAdopt,
sessionOutcomeAllowsDelivery,
type DecisionSessionOutcome,
} from "./rectification-agentic/core/rectification-decision";
import {
parseRectificationStepState,
type RectificationStepState,
} from "./rectification-agentic/v9/step-state.ts";
import {
parseRangeDelivery,
rangeDeliveryForSnapshot,
verificationMarkdownFromUnknown,
type RangeDeliveryProjection,
} from "./rectification-agentic/v9/divergence-panel.ts";
import { parseProductDelivery } from "./rectification-agentic/v9/product-delivery.ts";
export { parseRectificationStepState };
export type { RectificationStepState };
export type RectificationCandidate = Readonly<CandidatePosition & {
clusterIntervals?: readonly WindowInterval[];
candidateId: string;
rank: number;
time: string;
relativeSupport: number;
tiedMinuteCount: number;
clusterStart?: string;
clusterEnd?: string;
}>;
export type RectificationHouseRow = Readonly<{
house: number;
sign: string;
occupants: readonly string[];
}>;
export type RectificationHouseTable = Readonly<{
time: string;
lagna: string;
houses: readonly RectificationHouseRow[];
}>;
export type RectificationNatalRecast = Readonly<{
time: string;
lagna: string;
user_meaning: string;
unique_minute_claim: false;
confirmation_allowed: false;
}>;
export type RectificationInferenceMark = Readonly<{
time: string;
eliminated: boolean;
}>;
export type RectificationCandidateResult = Readonly<{
resultId: string;
candidates: readonly RectificationCandidate[];
overallConfidence: "low" | "medium" | "high";
selectionAllowed: boolean;
canAdopt: boolean;
confirmationAllowed: boolean;
decisionReceipt: Readonly<Record<string, unknown>> | null;
representativeTime: string | null;
selectedTime: string | null;
segmentSelection?: Readonly<{ resultId: string; candidateId: string; date: string; time: string; contract: "segment-v1" }> | null;
selectionKind: string | null;
houseTable: RectificationHouseTable | null;
houseTablesByTime: Readonly<Record<string, RectificationHouseTable>>;
natalRecast: RectificationNatalRecast | null;
techniqueAudit: readonly TechniqueAuditRow[];
windowTransitions: readonly WindowScanTransition[];
eventDashaLedger: readonly EventDashaLedgerRow[];
dashaAgreement: DashaAgreement | null;
lagnaContrast: LagnaContrast | null;
nakshatraBoundary: NakshatraBoundary | null;
precisionStage: PrecisionStage | null;
oosBlindPrompts: readonly OosBlindPrompt[];
confirmationGate: ConfirmationGate;
validated: boolean;
completionStatus: "provisional_range_user_stopped" | "validated_range" | "exact_minute_confirmed" | null;
sessionOutcome: DecisionSessionOutcome | null;
credibleRange: readonly [string, string] | null;
credibleIntervals?: readonly WindowInterval[];
declaredBirthDate?: string | null;
rangeDelivery: RangeDeliveryProjection | null;
verificationReportMarkdown: string | null;
/** Inference-layer minutes including eliminated ones. Empty when no inference_state. */
inferenceMarks?: readonly RectificationInferenceMark[];
/**
* `inference_state.answered_probe_count`, read verbatim off the wire. The
* count is the server's own; nothing here recounts answers or derives one
* from the transcript (VOICE.md rule 2: numbers come from the projection).
*/
answeredProbeCount?: number | null;
}>;
function record(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
function time(value: unknown): string | null {
if (typeof value !== "string") return null;
const normalized = value.slice(0, 5);
return /^(?:[01]\d|2[0-3]):[0-5]\d$/.test(normalized) ? normalized : null;
}
function text(value: unknown): string | null {
return typeof value === "string" ? value : null;
}
function finiteNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const deniedHouseTable = /lon|latitude|longitude|degree|score|fingerprint/i;
export function parseRectificationHouseTable(value: unknown): RectificationHouseTable | null {
const row = record(value);
const tableTime = time(row?.time);
const lagna = text(row?.lagna)?.trim() ?? "";
if (!row || !tableTime || !lagna || !Array.isArray(row.houses) || row.houses.length !== 12) return null;
const houses: RectificationHouseRow[] = [];
for (let index = 0; index < 12; index += 1) {
const house = record(row.houses[index]);
const houseNumber = finiteNumber(house?.house);
const sign = text(house?.sign)?.trim() ?? "";
if (
!house
|| houseNumber !== index + 1
|| !sign
|| deniedHouseTable.test(sign)
|| !Array.isArray(house.occupants)
) return null;
const occupants: string[] = [];
for (const occupant of house.occupants) {
const label = text(occupant)?.trim() ?? "";
if (!label) continue;
if (deniedHouseTable.test(label)) return null;
occupants.push(label);
}
houses.push({ house: houseNumber, sign, occupants });
}
return { time: tableTime, lagna, houses };
}
function houseTablesByTimeFromSnapshot(
snapshot: Record<string, unknown>,
receipt: Record<string, unknown> | null,
): Record<string, RectificationHouseTable> {
const raw = record(snapshot.houseTablesByTime)
?? record(snapshot.house_tables_by_time)
?? record(receipt?.house_tables_by_time);
const tables: Record<string, RectificationHouseTable> = {};
if (!raw) return tables;
for (const [key, value] of Object.entries(raw)) {
const clock = time(key);
const table = parseRectificationHouseTable(value);
if (!clock || !table) continue;
tables[clock] = table;
}
return tables;
}
function natalRecastFromSnapshot(
snapshot: Record<string, unknown>,
receipt: Record<string, unknown> | null,
table: RectificationHouseTable | null,
wantedMinute: string | null,
): RectificationNatalRecast | null {
const raw = record(snapshot.natalRecast) ?? record(snapshot.natal_recast) ?? record(receipt?.natal_recast);
const recastTime = table?.time ?? time(raw?.time);
const lagna = table?.lagna || (typeof raw?.lagna === "string" ? raw.lagna.trim() : "") || "";
if (!recastTime || !lagna || deniedHouseTable.test(lagna)) return null;
if (wantedMinute && recastTime !== wantedMinute) return null;
const meaning = natalRecastMeaning({ time: recastTime, lagna })
?? (typeof raw?.user_meaning === "string" && raw.user_meaning.trim() && !deniedHouseTable.test(raw.user_meaning)
? raw.user_meaning.trim().slice(0, 220)
: null);
if (!meaning) return null;
return {
time: recastTime,
lagna,
user_meaning: meaning,
unique_minute_claim: false,
confirmation_allowed: false,
};
}
export function natalRecastMeaning(table: Pick<RectificationHouseTable, "time" | "lagna"> | null): string | null {
if (!table) return null;
return `本命宫位已按 ${table.time} 重算(上升 ${table.lagna})。`;
}
export function houseTableAlignedToMinute(
wanted: string | null,
tablesByTime: Readonly<Record<string, RectificationHouseTable>>,
fallback: RectificationHouseTable | null,
): RectificationHouseTable | null {
if (wanted && tablesByTime[wanted]?.time === wanted) return tablesByTime[wanted];
if (fallback && wanted && fallback.time === wanted) return fallback;
if (!wanted) return fallback;
return null;
}
export function publicChartForMinute(
wanted: string | null,
snapshot: Record<string, unknown>,
receipt: Record<string, unknown> | null,
): {
houseTable: RectificationHouseTable | null;
natalRecast: RectificationNatalRecast | null;
} {
const tablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt);
const fallback = parseRectificationHouseTable(snapshot.houseTable)
?? parseRectificationHouseTable(snapshot.house_table)
?? parseRectificationHouseTable(receipt?.house_table);
const houseTable = houseTableAlignedToMinute(wanted, tablesByTime, fallback);
return {
houseTable,
natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, wanted),
};
}
export function workingRectificationHouseTable(
result: Pick<RectificationCandidateResult, "selectedTime" | "representativeTime" | "houseTable" | "houseTablesByTime">,
): RectificationHouseTable | null {
const wanted = result.selectedTime ?? result.representativeTime;
return houseTableAlignedToMinute(wanted, result.houseTablesByTime, result.houseTable);
}
function techniqueAuditFromSnapshot(
snapshot: Record<string, unknown>,
receipt: Record<string, unknown> | null,
): readonly TechniqueAuditRow[] {
return normalizeTechniqueAuditRows(snapshot.techniqueAudit)
?? normalizeTechniqueAuditRows(snapshot.technique_audit_table)
?? normalizeTechniqueAuditRows(receipt?.technique_audit_table)
?? [];
}
function receiptFromSnapshot(snapshot: Record<string, unknown>): Record<string, unknown> | null {
return record(snapshot.decisionReceipt) ?? record(snapshot.decision_receipt);
}
function strictAliasedBoolean(primary: unknown, alias: unknown): boolean {
if (primary === undefined && alias === undefined) return false;
if (primary !== undefined && typeof primary !== "boolean") return false;
if (alias !== undefined && typeof alias !== "boolean") return false;
if (primary !== undefined && alias !== undefined && primary !== alias) return false;
return (primary ?? alias) === true;
}
export function rectificationDecisionReceiptAllowsAdoption(value: unknown): boolean {
const ceiling = engineCapabilityCeilingFromReceipt(value);
return ceiling.acceptanceAllowed && ceiling.selectionAllowed;
}
function parseSessionOutcome(value: unknown): DecisionSessionOutcome | null {
if (typeof value !== "string") return null;
if (
sessionOutcomeAllowsAdopt(value)
|| value === "collect_evidence"
|| value === "discriminate_candidates"
|| value === "validate_holdout"
|| value === "provisional_range"
|| value === "completed_with_range"
|| value === "validated_range"
|| value === "exact_minute_confirmed"
) {
return value as DecisionSessionOutcome;
}
return null;
}
function parseCredibleRange(value: unknown): readonly [string, string] | null {
if (!Array.isArray(value) || value.length !== 2) return null;
const start = time(value[0]);
const end = time(value[1]);
if (!start || !end) return null;
return [start, end];
}
/**
* The server's count of answered discrimination probes. Read, never computed:
* the bar above the conversation may only show numbers the projection already
* carries, so this returns null rather than counting anything locally.
*/
function parseAnsweredProbeCount(receipt: Record<string, unknown> | null): number | null {
const state = record(receipt?.inference_state);
const value = state?.answered_probe_count;
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
}
function parseInferenceMarks(receipt: Record<string, unknown> | null): readonly RectificationInferenceMark[] {
const state = record(receipt?.inference_state);
if (!state || !Array.isArray(state.candidates)) return [];
const marks: RectificationInferenceMark[] = [];
const seen = new Set<string>();
for (const value of state.candidates) {
const row = record(value);
const clock = time(row?.time);
if (!clock || seen.has(clock)) continue;
seen.add(clock);
marks.push({
time: clock,
eliminated: row?.status === "eliminated",
});
}
return marks;
}
function parseSegmentSelection(value: unknown, resultId: string, selectedTime: string | null, candidates: readonly RectificationCandidate[]): RectificationCandidateResult["segmentSelection"] {
const row = record(value);
const date = text(row?.date);
const candidateId = text(row?.candidateId);
const selected = time(row?.time);
if (!row || row.contract !== "segment-v1" || row.resultId !== resultId || !date || !/^\d{4}-\d{2}-\d{2}$/.test(date)
|| !Number.isFinite(Date.parse(`${date}T00:00:00Z`)) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date
|| !selected || selected !== selectedTime || !candidateId || !candidates.some(candidate => candidate.candidateId === candidateId)) return null;
return { resultId, candidateId, date, time: selected, contract: "segment-v1" };
}
export function parseRectificationCandidateResult(value: unknown): RectificationCandidateResult | null {
const snapshot = record(value);
if (!snapshot || typeof snapshot.resultId !== "string") return null;
if (!Array.isArray(snapshot.candidates) || snapshot.candidates.length === 0) return null;
const candidates: RectificationCandidate[] = [];
for (const value of snapshot.candidates) {
const candidate = record(value);
const candidateId = candidate?.candidateId;
const candidateTime = time(candidate?.time);
const rank = finiteNumber(candidate?.rank);
const relativeSupport = finiteNumber(candidate?.relativeSupport);
const tiedMinuteCount = finiteNumber(candidate?.tiedMinuteCount);
if (
!candidate || typeof candidateId !== "string" || !uuidPattern.test(candidateId)
|| !candidateTime
|| rank === null || !Number.isInteger(rank) || rank < 1
|| relativeSupport === null || !Number.isInteger(relativeSupport) || relativeSupport < 0 || relativeSupport > 100
|| tiedMinuteCount === null || !Number.isInteger(tiedMinuteCount) || tiedMinuteCount < 1
) return null;
const clusterStart = time(candidate.clusterStart ?? candidate.cluster_start);
const clusterEnd = time(candidate.clusterEnd ?? candidate.cluster_end);
const position = readCandidatePosition(candidate);
if ((candidate.candidate_date !== undefined || isDatedScoringAlgorithmVersion(snapshot.algorithmVersion)) && !position) return null;
candidates.push({
...(position ?? {}),
...(position?.cluster_intervals ? { clusterIntervals: position.cluster_intervals } : {}),
candidateId,
rank,
time: candidateTime,
relativeSupport,
tiedMinuteCount,
...(clusterStart ? { clusterStart } : {}),
...(clusterEnd ? { clusterEnd } : {}),
});
}
const receipt = receiptFromSnapshot(snapshot);
const declaredProductDelivery = snapshot.productDelivery !== undefined ? snapshot.productDelivery : snapshot.product_delivery;
const productDelivery = declaredProductDelivery === null ? null
: parseProductDelivery(declaredProductDelivery) ?? parseProductDelivery(receipt?.product_delivery) ?? undefined;
const receiptAllowsAdoption = rectificationDecisionReceiptAllowsAdoption(receipt);
const snapshotSelectionAllowed = strictAliasedBoolean(snapshot.selectionAllowed, snapshot.selection_allowed);
const snapshotCanAdopt = strictAliasedBoolean(snapshot.canAdopt, snapshot.can_adopt);
const refinement = refinementFromDecisionReceipt(receipt);
const windowScan = parseWindowScan(receipt?.window_scan) ?? parseWindowScan(snapshot.window_scan);
const selectedTime = time(snapshot.selectedTime);
const representativeTime = time(snapshot.representativeTime);
const houseTablesByTime = houseTablesByTimeFromSnapshot(snapshot, receipt);
const fallbackHouseTable = parseRectificationHouseTable(snapshot.houseTable)
?? parseRectificationHouseTable(snapshot.house_table)
?? parseRectificationHouseTable(receipt?.house_table);
const houseTable = houseTableAlignedToMinute(
selectedTime ?? representativeTime,
houseTablesByTime,
fallbackHouseTable,
);
return {
resultId: snapshot.resultId,
candidates,
overallConfidence: snapshot.overallConfidence === "high" || snapshot.overallConfidence === "medium"
? snapshot.overallConfidence
: "low",
selectionAllowed: snapshotSelectionAllowed && receiptAllowsAdoption,
canAdopt: snapshotCanAdopt && receiptAllowsAdoption,
confirmationAllowed: snapshot.confirmationAllowed === true,
decisionReceipt: receipt,
representativeTime,
selectedTime,
segmentSelection: parseSegmentSelection(snapshot.segmentSelection, snapshot.resultId, selectedTime, candidates),
selectionKind: text(snapshot.selectionKind),
houseTable,
houseTablesByTime,
natalRecast: natalRecastFromSnapshot(snapshot, receipt, houseTable, selectedTime ?? representativeTime),
techniqueAudit: techniqueAuditFromSnapshot(snapshot, receipt),
windowTransitions: (windowScan?.transitions ?? parseWindowScanTransitions(snapshot.window_scan)).filter((row) => !row.segment_start),
eventDashaLedger: refinement.event_dasha_ledger,
dashaAgreement: refinement.dasha_agreement,
lagnaContrast: refinement.lagna_contrast,
nakshatraBoundary: refinement.nakshatra_boundary,
precisionStage: refinement.precision_stage,
oosBlindPrompts: refinement.oos_blind_prompts,
confirmationGate: buildConfirmationGate({
engineConfirmationAllowed: snapshot.confirmationAllowed === true,
candidates,
decisionReceipt: receipt,
}),
validated: snapshot.validated === true,
completionStatus: snapshot.completionStatus === "provisional_range_user_stopped"
|| snapshot.completionStatus === "validated_range"
|| snapshot.completionStatus === "exact_minute_confirmed"
|| snapshot.completion_status === "provisional_range_user_stopped"
|| snapshot.completion_status === "validated_range"
|| snapshot.completion_status === "exact_minute_confirmed"
? (snapshot.completionStatus ?? snapshot.completion_status) as RectificationCandidateResult["completionStatus"]
: null,
sessionOutcome: parseSessionOutcome(snapshot.sessionOutcome ?? snapshot.session_outcome),
declaredBirthDate: receipt?.candidate_window_contract === "dated-v1" ? text(receipt.declared_birth_date) : null,
credibleRange: parseCredibleRange(snapshot.credibleRange ?? snapshot.credible_range),
credibleIntervals: parseWindowIntervals(snapshot.credibleIntervals ?? snapshot.credible_intervals
?? record(receipt?.inference_state)?.credible_intervals) ?? undefined,
rangeDelivery: parseRangeDelivery(snapshot.rangeDelivery ?? snapshot.range_delivery, productDelivery)
?? rangeDeliveryForSnapshot({ ...snapshot, productDelivery }),
verificationReportMarkdown: verificationMarkdownFromUnknown(snapshot.skill_verification_report)
?? verificationMarkdownFromUnknown(snapshot.skillVerificationReport)
?? verificationMarkdownFromUnknown(record(snapshot.rangeDelivery ?? snapshot.range_delivery)?.verification_markdown),
inferenceMarks: parseInferenceMarks(receipt),
answeredProbeCount: parseAnsweredProbeCount(receipt),
};
}
export function canRenderRectificationSelectionCards(
result: RectificationCandidateResult | null,
): boolean {
return Boolean(
result?.selectionAllowed
&& result.canAdopt
&& rectificationDecisionReceiptAllowsAdoption(result.decisionReceipt),
);
}
export function canShowRectificationSelectionCards(
result: RectificationCandidateResult | null,
): boolean {
if (!result || !sessionOutcomeAllowsDelivery(result.sessionOutcome)) return false;
if (sessionOutcomeAllowsAdopt(result.sessionOutcome)) {
return canRenderRectificationSelectionCards(result);
}
return result.selectionAllowed === true;
}
export function canShowRectificationReadonlyRange(
result: RectificationCandidateResult | null,
): boolean {
if (!result?.credibleRange) return false;
const outcome = result.sessionOutcome;
if (sessionOutcomeAllowsDelivery(outcome)) {
return result.selectionAllowed !== true;
}
return outcome === "collect_evidence" || outcome === "discriminate_candidates";
}
export function isRecommendedRectificationCandidate(
result: RectificationCandidateResult,
candidate: RectificationCandidate,
): boolean {
if (result.selectedTime || !result.selectionAllowed || result.representativeTime !== candidate.time) {
return false;
}
const ranked = [...result.candidates].sort((left, right) => right.relativeSupport - left.relativeSupport);
const lead = (ranked[0]?.relativeSupport ?? 0) - (ranked[1]?.relativeSupport ?? 0);
return lead >= MIN_SEPARATION_LEAD;
}