Files
Jyotisha/frontend/src/lib/rectification-agentic/core/compose-receipt.ts
T
66f087b588 feat(rectification): submit varga-resolution implementation for review
Review-only snapshot for BUG-1115 through BUG-1117; not merge-ready. New opening and append-turn PostgreSQL permission failures remain blocked. Persisted joint replay has zero completed questions; segment ordering remains off by default. The existing offline replay JSON is retained stale and unchanged after a denied overwrite, including its CRLF line endings. Browser/provider validation and final serial gates remain pending. No deployment, role permission changes, or staging/main push.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 08:04:04 +08:00

272 lines
14 KiB
TypeScript

import { candidateSetId } from "./candidate-set-id.ts";
import { readCandidatePosition, parseWindowIntervals, type CandidatePosition } from "./candidate-window.ts";
import { INFERENCE_ALGORITHM_VERSION, type InferenceState } from "./types.ts";
import { buildSegmentSummary, parseAscendantSignature, parseSegmentSummary, TARGET_CHARTS, type SegmentMinute } from "./segment-summary.ts";
export type InferenceTransitionSnapshot = Readonly<{
id?: string;
resultId: string;
revision: number;
probeId: string | null;
reason: string;
idempotent?: boolean;
decisionStateFingerprint: string;
inferenceState: InferenceState;
posteriorBefore: Readonly<Record<string, number>>;
posteriorAfter: Readonly<Record<string, number>>;
scoreDeltas: Readonly<Record<string, number>>;
entropyBefore?: number | null;
entropyAfter?: number | null;
eliminatedCandidateIds?: readonly string[];
roundKind?: "informative" | "low_information" | "tie_break" | null;
}>;
const CLOCK = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
const PHASES = new Set([
"intake", "event_collection", "candidate_generation", "candidate_scoring",
"discrimination", "holdout_validation", "completed", "stopped",
]);
const RESULT_STATUSES = new Set([
"converged", "credible_range", "completed_with_range", "insufficient_evidence",
"max_rounds_reached", "validation_failed", "discriminating",
]);
const CANDIDATE_STATUSES = new Set(["active", "eliminated", "winner", "equivalent"]);
const EVENT_PRECISIONS = new Set(["day", "month", "year", "unknown"]);
const EVENT_USAGES = new Set(["training", "holdout", "unused"]);
const ANSWER_CLASSES = new Set(["yes", "weak_yes", "no", "unsure"]);
const PROBE_CHOICE_KINDS = new Set(["existence", "varga_style", "event_quality"]);
const ANSWER_SOURCES = new Set(["choice", "evidence", "declined"]);
const ROUND_KINDS = new Set(["informative", "low_information", "tie_break"]);
const SCORE_DIRECTIONS = new Set(["support", "weak_support", "neutral", "weak_conflict", "conflict"]);
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isClockRange(value: unknown, dated = false): value is readonly [string, string] {
return Array.isArray(value)
&& value.length === 2
&& typeof value[0] === "string"
&& typeof value[1] === "string"
&& CLOCK.test(value[0])
&& CLOCK.test(value[1])
&& (dated || value[0] <= value[1]);
}
function isFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
function isNumberRecord(value: unknown): boolean {
return isRecord(value) && Object.values(value).every(isFiniteNumber);
}
function isDirectionRecord(value: unknown): boolean {
return isRecord(value) && Object.values(value).every((item) => (
typeof item === "string" && SCORE_DIRECTIONS.has(item)
));
}
function isInferenceEvent(value: unknown): boolean {
return isRecord(value)
&& typeof value.id === "string" && value.id.length > 0
&& typeof value.domain === "string" && value.domain.length > 0
&& (value.year === null || Number.isInteger(value.year))
&& typeof value.precision === "string" && EVENT_PRECISIONS.has(value.precision)
&& typeof value.usage === "string" && EVENT_USAGES.has(value.usage);
}
function isProbeOutcome(value: unknown): boolean {
return isRecord(value)
&& typeof value.answer_class === "string" && ANSWER_CLASSES.has(value.answer_class)
&& isStringArray(value.supports)
&& isStringArray(value.conflicts);
}
function isConflictProbe(value: unknown): boolean {
return isRecord(value)
&& typeof value.id === "string" && value.id.length > 0
&& typeof value.semantic_key === "string" && value.semantic_key.length > 0
&& typeof value.candidate_split_hash === "string" && value.candidate_split_hash.length > 0
&& typeof value.domain === "string" && value.domain.length > 0
&& Number.isInteger(value.year)
&& typeof value.question === "string" && value.question.length > 0
&& isStringArray(value.candidate_ids)
&& Array.isArray(value.expected_outcomes) && value.expected_outcomes.every(isProbeOutcome)
&& isFiniteNumber(value.information_gain)
&& typeof value.source === "string" && value.source.length > 0
&& (value.choice_kind === undefined
|| (typeof value.choice_kind === "string" && PROBE_CHOICE_KINDS.has(value.choice_kind)))
&& (value.carried === undefined || value.carried === true || value.carried === false)
&& (value.outcome_by_minute === undefined || isDirectionRecord(value.outcome_by_minute));
}
function isProbeAnswer(value: unknown): boolean {
return isRecord(value)
&& typeof value.probe_id === "string" && value.probe_id.length > 0
&& typeof value.semantic_key === "string" && value.semantic_key.length > 0
&& typeof value.candidate_split_hash === "string" && value.candidate_split_hash.length > 0
&& typeof value.answer_class === "string" && ANSWER_CLASSES.has(value.answer_class)
&& typeof value.classified_from === "string" && ANSWER_SOURCES.has(value.classified_from);
}
function isRoundTrace(value: unknown): boolean {
return isRecord(value)
&& Number.isInteger(value.round) && Number(value.round) >= 0
&& typeof value.phase === "string" && PHASES.has(value.phase)
&& (value.probe_id === null || typeof value.probe_id === "string")
&& isNumberRecord(value.scores_before)
&& isNumberRecord(value.scores_after)
&& (value.score_deltas === undefined || isNumberRecord(value.score_deltas))
&& isFiniteNumber(value.entropy_before)
&& isFiniteNumber(value.entropy_after)
&& isStringArray(value.eliminated_ids)
&& (value.winner_id === null || typeof value.winner_id === "string")
&& typeof value.kind === "string" && ROUND_KINDS.has(value.kind);
}
function isInferenceCandidate(value: unknown): boolean {
if (!isRecord(value)
|| typeof value.id !== "string" || value.id.length === 0
|| typeof value.time !== "string" || !CLOCK.test(value.time)
|| !isClockRange(value.cluster_range, value.candidate_date !== undefined)
|| (value.candidate_date !== undefined ? !readCandidatePosition(value)
: value.time < value.cluster_range[0] || value.time > value.cluster_range[1])
|| !isFiniteNumber(value.prior_score)
|| !isFiniteNumber(value.posterior_score)
|| !isFiniteNumber(value.probability) || value.probability < 0 || value.probability > 1
|| typeof value.status !== "string" || !CANDIDATE_STATUSES.has(value.status)
|| !Number.isInteger(value.rank) || Number(value.rank) < 1
|| !Number.isInteger(value.strong_conflict_count) || Number(value.strong_conflict_count) < 0) return false;
return true;
}
function canonicalSegmentJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalSegmentJson).join(",")}]`;
if (isRecord(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalSegmentJson(value[key])}`).join(",")}}`;
return JSON.stringify(value) ?? "null";
}
export function asInferenceState(value: unknown): InferenceState | null {
if (!isRecord(value)
|| value.algorithm_version !== INFERENCE_ALGORITHM_VERSION
|| typeof value.candidate_set_id !== "string" || value.candidate_set_id.trim().length === 0
|| !Number.isInteger(value.revision) || Number(value.revision) < 0
|| typeof value.phase !== "string" || !PHASES.has(value.phase)
|| typeof value.result_status !== "string" || !RESULT_STATUSES.has(value.result_status)
|| typeof value.range_start !== "string" || !CLOCK.test(value.range_start)
|| typeof value.range_end !== "string" || !CLOCK.test(value.range_end)
|| (!value.candidate_set_id.startsWith("dated-v1:") && value.range_start > value.range_end)
|| !Array.isArray(value.candidates) || value.candidates.length === 0
|| !value.candidates.every(isInferenceCandidate)
|| new Set(value.candidates.map((item) => (item as Readonly<Record<string, unknown>>).id)).size !== value.candidates.length
|| new Set(value.candidates.map((item) => (item as Readonly<Record<string, unknown>>).time)).size !== value.candidates.length
|| value.candidate_set_id !== candidateSetId(
value.range_start,
value.range_end,
value.candidates.map((item) => (item as Readonly<Record<string, unknown>>).time as string),
value.candidates as (CandidatePosition & { time: string })[],
)
|| !Array.isArray(value.events) || !value.events.every(isInferenceEvent)
|| !Array.isArray(value.probes) || !value.probes.every(isConflictProbe)
|| !Array.isArray(value.answered_probes) || !value.answered_probes.every(isProbeAnswer)
|| !Array.isArray(value.rounds) || !value.rounds.every(isRoundTrace)
|| !(value.last_inference_round === undefined
|| value.last_inference_round === null
|| isRoundTrace(value.last_inference_round))
|| !isFiniteNumber(value.entropy)
|| !(value.representative_time === null
|| (typeof value.representative_time === "string" && CLOCK.test(value.representative_time)))
|| !(value.credible_range === null || isClockRange(value.credible_range, value.candidate_set_id.startsWith("dated-v1:")))
|| (value.candidate_set_id.startsWith("dated-v1:") && (
!value.candidates.every((item) => readCandidatePosition(item as Record<string, unknown>))
|| !parseWindowIntervals(value.credible_intervals)
))) return null;
const state = value as InferenceState;
const candidateMetadataValid = state.candidates.every((candidate) => (
(candidate.raw_prior_score === undefined || isFiniteNumber(candidate.raw_prior_score))
&& (candidate.raw_posterior_score === undefined || isFiniteNumber(candidate.raw_posterior_score))
&& (candidate.raw_eliminated === undefined || typeof candidate.raw_eliminated === "boolean")
&& (candidate.signature === undefined || Boolean(parseAscendantSignature(candidate.signature)))
&& (candidate.cluster_times === undefined || (isStringArray(candidate.cluster_times)
&& candidate.cluster_times.every((time) => CLOCK.test(time))))
));
if (candidateMetadataValid && value.segment_summary === undefined && value.segment_minutes === undefined) return state;
const summary = parseSegmentSummary(value.segment_summary);
const minutes = value.segment_minutes;
let valid = Boolean(candidateMetadataValid && summary && Array.isArray(minutes) && minutes.length && minutes.every((minute) =>
isRecord(minute) && Number.isInteger(minute.offset) && Number(minute.offset) >= 0
&& typeof minute.time === "string" && CLOCK.test(minute.time) && parseAscendantSignature(minute.signs)
&& (minute.date === undefined || (typeof minute.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(minute.date)
&& Number.isFinite(Date.parse(`${minute.date}T00:00:00Z`))
&& new Date(`${minute.date}T00:00:00Z`).toISOString().slice(0, 10) === minute.date)))
&& (value.segment_targets === undefined || (Array.isArray(value.segment_targets)
&& value.segment_targets.every((chart) => TARGET_CHARTS.includes(chart))
&& JSON.stringify(value.segment_targets) === JSON.stringify(summary?.targets)))
&& typeof value.segment_scan_complete === "boolean" && typeof value.segment_order_enabled === "boolean"
&& state.candidates.every((candidate) => candidate.raw_posterior_score !== undefined && typeof candidate.raw_eliminated === "boolean"));
if (valid && summary) {
const clock = (time: string) => Number(time.slice(0, 2)) * 60 + Number(time.slice(3, 5));
const delivered = state.credible_intervals?.length
? state.credible_intervals.map((part) => [part.start_offset_minutes, part.end_offset_minutes] as const)
: state.credible_range ? [[(clock(state.credible_range[0]) - clock(state.range_start) + 1440) % 1440,
(clock(state.credible_range[1]) - clock(state.range_start) + 1440) % 1440] as const] : [];
const expected = buildSegmentSummary({ minutes: minutes as SegmentMinute[], targets: summary.targets,
windowMinutes: summary.window_minutes, scanComplete: state.segment_scan_complete === true, deliveredOffsets: delivered,
candidates: state.candidates.map((candidate) => ({ time: candidate.time, score: candidate.raw_posterior_score!,
cluster_times: candidate.cluster_times, eliminated: candidate.raw_eliminated })) });
valid = canonicalSegmentJson(summary) === canonicalSegmentJson(expected);
}
if (valid) return state;
// Optional chart metadata must never erase the authoritative frozen inference.
const clean = { ...state };
if (!candidateMetadataValid) clean.candidates = state.candidates.map((candidate) => {
const row = { ...candidate };
delete row.raw_prior_score;
delete row.raw_posterior_score;
delete row.raw_eliminated;
delete row.signature;
delete row.cluster_times;
return row;
});
delete clean.segment_summary;
delete clean.segment_minutes;
delete clean.segment_targets;
delete clean.segment_scan_complete;
delete clean.segment_order_enabled;
return clean;
}
/**
* Overlay the latest inference revision onto an immutable engine receipt.
* Only apply when the transition was recorded against this engine result.
*/
export function composeInferenceReceipt(
engineReceipt: Readonly<Record<string, unknown>> | null | undefined,
transition: InferenceTransitionSnapshot | null | undefined,
resultId?: string | null,
): Record<string, unknown> {
const receipt = engineReceipt && typeof engineReceipt === "object" && !Array.isArray(engineReceipt)
? { ...engineReceipt }
: {};
if (!transition) return receipt;
if (resultId && transition.resultId && transition.resultId !== resultId) return receipt;
const inferenceState = asInferenceState(transition.inferenceState);
if (!inferenceState || transition.revision !== inferenceState.revision) return receipt;
return {
...receipt,
inference_state: inferenceState,
decision_state_fingerprint: transition.decisionStateFingerprint,
};
}
export function previousInferenceFromReceipt(
receipt: Readonly<Record<string, unknown>> | null | undefined,
): InferenceState | null {
return asInferenceState(receipt?.inference_state);
}