fix(web): count only training events for discrimination and split user-stop from validated range

Three collected events with a reserved holdout were stalling because the discriminator door counted holdout. Public selection_allowed still had snapshot fallbacks, and health only proved the image SHA.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-26 19:36:04 +08:00
co-authored by Cursor
parent a0ce55f066
commit b1173f7245
37 changed files with 1362 additions and 234 deletions
+65
View File
@@ -1,6 +1,14 @@
import { NextResponse } from "next/server";
import { getTruthSourceRuntimeIdentity } from "@/lib/truth-source-runtime-identity";
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
import {
databaseHealthFromFilenames,
RECTIFICATION_CONTRACT_VERSION,
REQUIRED_RECTIFICATION_MIGRATIONS,
type DatabaseHealth,
} from "@/lib/health-database-contract";
export const runtime = "nodejs";
type Check = {
status: "ok" | "degraded" | "blocked";
@@ -42,6 +50,56 @@ async function jyotishApiCheck(): Promise<Check> {
}
}
async function rectificationMigrationCheck(): Promise<{
check: Check;
database: DatabaseHealth;
}> {
const empty: DatabaseHealth = {
latestMigration: null,
rectificationContractVersion: RECTIFICATION_CONTRACT_VERSION,
requiredMigrationsPresent: false,
missingMigrations: [...REQUIRED_RECTIFICATION_MIGRATIONS],
};
if (process.env.AUTH_PROVIDER?.trim() !== "self-hosted") {
return { check: { status: "ok", message: "skipped_non_local" }, database: empty };
}
const url = process.env.APP_DATABASE_URL?.trim();
if (!url) {
return { check: { status: "blocked", message: "missing:APP_DATABASE_URL" }, database: empty };
}
const started = Date.now();
const { Client } = await import("pg");
const client = new Client({ connectionString: url, connectionTimeoutMillis: 2000 });
try {
await client.connect();
const result = await client.query<{ filename: string }>(
"select filename from migration.schema_migrations order by filename",
);
const database = databaseHealthFromFilenames(result.rows.map((row) => row.filename));
return {
check: database.requiredMigrationsPresent
? { status: "ok", latencyMs: Date.now() - started }
: {
status: "blocked",
message: `missing_migrations:${database.missingMigrations.join(",")}`,
latencyMs: Date.now() - started,
},
database,
};
} catch (error) {
return {
check: {
status: "blocked",
message: error instanceof Error ? error.name : "database_migration_query_failed",
latencyMs: Date.now() - started,
},
database: empty,
};
} finally {
await client.end().catch(() => undefined);
}
}
async function modelCatalogCheck(): Promise<Check> {
const catalog = await loadLanguageModelCatalog();
const defaults = catalog.models.filter((model) => model.isDefault && model.id === catalog.defaultModelId);
@@ -74,12 +132,14 @@ export async function GET() {
supabasePublicConfig: envCheck(["NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY"]),
supabaseServiceRole: envCheck(["SUPABASE_SERVICE_ROLE_KEY"]),
};
const migrations = await rectificationMigrationCheck();
const checks = {
web: { status: "ok" } satisfies Check,
...databaseChecks,
modelProviderEncryption: envCheck(["MODEL_PROVIDER_CONFIG_ENCRYPTION_KEY"]),
modelCatalog: await modelCatalogCheck(),
jyotishApi: await jyotishApiCheck(),
rectificationMigrations: migrations.check,
researchTruthSource: {
status: truthSourceIdentity.status,
message: truthSourceIdentity.mountStatus === "mounted" ? undefined : truthSourceIdentity.mountStatus,
@@ -93,6 +153,11 @@ export async function GET() {
deployment: {
gitCommit,
},
database: {
latestMigration: migrations.database.latestMigration,
rectificationContractVersion: migrations.database.rectificationContractVersion,
requiredMigrationsPresent: migrations.database.requiredMigrationsPresent,
},
truthSource: truthSourceIdentity,
checks,
},
@@ -3,6 +3,7 @@ import { z } from "zod";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
evidenceLedgerFingerprint,
loadV9CaseDossier,
loadV9CaseSkillIdentityStatus,
loadV9TurnReceipt,
@@ -10,8 +11,9 @@ import {
RectificationToolServiceError,
type V9CaseDossier,
} from "@/lib/rectification-agentic/v9/tool-service";
import { choiceCardFromCaseDossier } from "@/lib/rectification-agentic/v9/interview-state";
import { choiceCardFromCaseDossier, decideFromDossier, overlayPublicDecision } from "@/lib/rectification-agentic/v9/interview-state";
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
import { publicDecisionFields } from "@/lib/rectification-agentic/core/rectification-decision";
export const runtime = "nodejs";
@@ -83,6 +85,9 @@ function dossierResponse(
receipts: Array<Awaited<ReturnType<typeof loadV9TurnReceipt>>>,
skillIdentity: Awaited<ReturnType<typeof loadV9CaseSkillIdentityStatus>>,
) {
const decision = decideFromDossier(dossier, {
currentEvidenceFingerprint: evidenceLedgerFingerprint(dossier.evidence),
});
return {
case: {
case_id: dossier.case.caseId,
@@ -109,7 +114,8 @@ function dossierResponse(
receipt: turnReceipt(turn.id, receipts),
})),
evidence: dossier.evidence,
latest_result: dossier.latestResult,
latest_result: dossier.latestResult ? overlayPublicDecision(dossier.latestResult, decision) : null,
interview: publicDecisionFields(decision),
choice_card: choiceCardFromCaseDossier(dossier),
};
}